From 122a4dc9db074f7bd14421e1d8b9244a05318da7 Mon Sep 17 00:00:00 2001 From: Michael reda Date: Wed, 5 Apr 2023 12:14:06 +0200 Subject: [PATCH 001/189] add sync to specific projects or listen only --- openpype/modules/kitsu/kitsu_module.py | 16 ++++++++++-- .../modules/kitsu/utils/update_op_with_zou.py | 25 +++++++++++++++++-- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/openpype/modules/kitsu/kitsu_module.py b/openpype/modules/kitsu/kitsu_module.py index b91373af20..f4e3dd5691 100644 --- a/openpype/modules/kitsu/kitsu_module.py +++ b/openpype/modules/kitsu/kitsu_module.py @@ -124,19 +124,31 @@ def push_to_zou(login, password): @cli_main.command() +@click.option("-prjs", "--projects", envvar="SYNC_PROJECTS", help="Sync specific kitsu projects") @click.option("-l", "--login", envvar="KITSU_LOGIN", help="Kitsu login") @click.option( "-p", "--password", envvar="KITSU_PWD", help="Password for kitsu username" ) -def sync_service(login, password): +def sync_service(login, password, projects="^"): """Synchronize openpype database from Zou sever database. Args: login (str): Kitsu user login password (str): Kitsu user password + projects (str): specific kitsu projects + + SYNC_PROJECTS: + *: all projects + ^: dont sync any project just listen + "project01 project02 ...": to choose custom projects + + """ from .utils.update_op_with_zou import sync_all_projects from .utils.sync_service import start_listeners - sync_all_projects(login, password) + projects = projects.strip() + projects = projects.split(' ') + + sync_all_projects(login, password, specific_projects=projects) start_listeners(login, password) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index 4f4f0810bc..a397198a13 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -359,7 +359,7 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: def sync_all_projects( - login: str, password: str, ignore_projects: list = None + login: str, password: str, ignore_projects: list = None, specific_projects: list = None ): """Update all OP projects in DB with Zou data. @@ -367,6 +367,7 @@ def sync_all_projects( login (str): Kitsu user login password (str): Kitsu user password ignore_projects (list): List of unsynced project names + specific_projects (list): List of synced project names Raises: gazu.exception.AuthFailedException: Wrong user login and/or password """ @@ -381,7 +382,27 @@ def sync_all_projects( dbcon = AvalonMongoDB() dbcon.install() all_projects = gazu.project.all_projects() - for project in all_projects: + + + project_to_sync = [] + if specific_projects == ['*']: + project_to_sync = all_projects + + elif specific_projects == ['^']: + return + + elif isinstance(specific_projects, list): + all_kitsu_projects = {p['name']: p for p in all_projects} + for proj_name in specific_projects: + if proj_name in all_kitsu_projects: + project_to_sync.append(all_kitsu_projects[proj_name]) + else: + log.info(f'`{proj_name}` project does not exists in kitsu.' + f' Please make sure you write the project correctly.') + else: + return + + for project in project_to_sync: if ignore_projects and project["name"] in ignore_projects: continue sync_project_from_kitsu(dbcon, project) From 3c801ca1187ac3d1cf1d0da50cf27eceaae5fa30 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 18 Apr 2023 22:39:07 +0200 Subject: [PATCH 002/189] Fix imports --- openpype/hosts/maya/api/setdress.py | 8 +++----- openpype/hosts/maya/plugins/load/load_assembly.py | 15 ++++++--------- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/maya/api/setdress.py b/openpype/hosts/maya/api/setdress.py index 159bfe9eb3..0bb1f186eb 100644 --- a/openpype/hosts/maya/api/setdress.py +++ b/openpype/hosts/maya/api/setdress.py @@ -28,7 +28,9 @@ from openpype.pipeline import ( ) from openpype.hosts.maya.api.lib import ( matrix_equals, - unique_namespace + unique_namespace, + get_container_transforms, + DEFAULT_MATRIX ) log = logging.getLogger("PackageLoader") @@ -183,8 +185,6 @@ def _add(instance, representation_id, loaders, namespace, root="|"): """ - from openpype.hosts.maya.lib import get_container_transforms - # Process within the namespace with namespaced(namespace, new=False) as namespace: @@ -379,8 +379,6 @@ def update_scene(set_container, containers, current_data, new_data, new_file): """ - from openpype.hosts.maya.lib import DEFAULT_MATRIX, get_container_transforms - set_namespace = set_container['namespace'] project_name = legacy_io.active_project() diff --git a/openpype/hosts/maya/plugins/load/load_assembly.py b/openpype/hosts/maya/plugins/load/load_assembly.py index 902f38695c..275f21be5d 100644 --- a/openpype/hosts/maya/plugins/load/load_assembly.py +++ b/openpype/hosts/maya/plugins/load/load_assembly.py @@ -1,8 +1,14 @@ +import maya.cmds as cmds + from openpype.pipeline import ( load, remove_container ) +from openpype.hosts.maya.api.pipeline import containerise +from openpype.hosts.maya.api.lib import unique_namespace +from openpype.hosts.maya.api import setdress + class AssemblyLoader(load.LoaderPlugin): @@ -16,9 +22,6 @@ class AssemblyLoader(load.LoaderPlugin): def load(self, context, name, namespace, data): - from openpype.hosts.maya.api.pipeline import containerise - from openpype.hosts.maya.api.lib import unique_namespace - asset = context['asset']['name'] namespace = namespace or unique_namespace( asset + "_", @@ -26,8 +29,6 @@ class AssemblyLoader(load.LoaderPlugin): suffix="_", ) - from openpype.hosts.maya.api import setdress - containers = setdress.load_package( filepath=self.fname, name=name, @@ -50,15 +51,11 @@ class AssemblyLoader(load.LoaderPlugin): def update(self, container, representation): - from openpype import setdress return setdress.update_package(container, representation) def remove(self, container): """Remove all sub containers""" - from openpype import setdress - import maya.cmds as cmds - # Remove all members member_containers = setdress.get_contained_containers(container) for member_container in member_containers: From 557cbb72cefa955f75fa6c3f1df7fe38665e1c99 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 27 Apr 2023 11:26:57 +0200 Subject: [PATCH 003/189] :recycle: escape rootless path --- openpype/modules/deadline/plugins/publish/submit_publish_job.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index f80bd40133..6ee100ddb4 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -275,7 +275,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): args = [ "--headless", 'publish', - rootless_metadata_path, + '"{}"'.format(rootless_metadata_path), "--targets", "deadline", "--targets", "farm" ] From fdc94f3b88a1eaaa1509ec5e802d82e261623034 Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov Date: Fri, 28 Apr 2023 17:50:55 +0300 Subject: [PATCH 004/189] add vscode workspace to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 18e7cd7bf2..a565c57b54 100644 --- a/.gitignore +++ b/.gitignore @@ -85,6 +85,7 @@ openpype/premiere/ppro/js/debug.log .env dump.sql test_localsystem.txt +*.code-workspace # website ########## From 015f13bb908e48db077c528e0388ef4c31a39d7a Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov Date: Fri, 28 Apr 2023 17:57:00 +0300 Subject: [PATCH 005/189] better variable naming for utils --- openpype/hosts/resolve/utils.py | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/openpype/hosts/resolve/utils.py b/openpype/hosts/resolve/utils.py index 5881f153ae..8e5dd9a188 100644 --- a/openpype/hosts/resolve/utils.py +++ b/openpype/hosts/resolve/utils.py @@ -8,30 +8,30 @@ RESOLVE_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) def setup(env): log = Logger.get_logger("ResolveSetup") scripts = {} - us_env = env.get("RESOLVE_UTILITY_SCRIPTS_SOURCE_DIR") - us_dir = env["RESOLVE_UTILITY_SCRIPTS_DIR"] + util_scripts_env = env.get("RESOLVE_UTILITY_SCRIPTS_SOURCE_DIR") + util_scripts_dir = env["RESOLVE_UTILITY_SCRIPTS_DIR"] - us_paths = [os.path.join( + util_scripts_paths = [os.path.join( RESOLVE_ROOT_DIR, "utility_scripts" )] # collect script dirs - if us_env: - log.info("Utility Scripts Env: `{}`".format(us_env)) - us_paths = us_env.split( - os.pathsep) + us_paths + if util_scripts_env: + log.info("Utility Scripts Env: `{}`".format(util_scripts_env)) + util_scripts_paths = util_scripts_env.split( + os.pathsep) + util_scripts_paths # collect scripts from dirs - for path in us_paths: + for path in util_scripts_paths: scripts.update({path: os.listdir(path)}) - log.info("Utility Scripts Dir: `{}`".format(us_paths)) + log.info("Utility Scripts Dir: `{}`".format(util_scripts_paths)) log.info("Utility Scripts: `{}`".format(scripts)) # make sure no script file is in folder - for s in os.listdir(us_dir): - path = os.path.join(us_dir, s) + for script in os.listdir(util_scripts_dir): + path = os.path.join(util_scripts_dir, script) log.info("Removing `{}`...".format(path)) if os.path.isdir(path): shutil.rmtree(path, onerror=None) @@ -39,12 +39,10 @@ def setup(env): os.remove(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.join(d, s) - dst = os.path.join(us_dir, s) + for directory, scripts in scripts.items(): + for script in scripts: + src = os.path.join(directory, script) + dst = os.path.join(util_scripts_dir, script) log.info("Copying `{}` to `{}`...".format(src, dst)) if os.path.isdir(src): shutil.copytree( From 7cc08d026a1d812ee62c5f12ba67dbf6c2670c60 Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov Date: Fri, 28 Apr 2023 17:57:29 +0300 Subject: [PATCH 006/189] upse pathlib instead of os.path, some cleanup --- .../hosts/resolve/hooks/pre_resolve_setup.py | 55 ++++++++----------- 1 file changed, 23 insertions(+), 32 deletions(-) diff --git a/openpype/hosts/resolve/hooks/pre_resolve_setup.py b/openpype/hosts/resolve/hooks/pre_resolve_setup.py index 8574b3ad01..3144a60312 100644 --- a/openpype/hosts/resolve/hooks/pre_resolve_setup.py +++ b/openpype/hosts/resolve/hooks/pre_resolve_setup.py @@ -1,4 +1,5 @@ import os +from pathlib import Path import platform from openpype.lib import PreLaunchHook from openpype.hosts.resolve.utils import setup @@ -16,10 +17,10 @@ class ResolvePrelaunch(PreLaunchHook): def execute(self): current_platform = platform.system().lower() - PROGRAMDATA = self.launch_context.env.get("PROGRAMDATA", "") - RESOLVE_SCRIPT_API_ = { + programdata = self.launch_context.env.get("PROGRAMDATA", "") + resolve_script_api_locations = { "windows": ( - f"{PROGRAMDATA}/Blackmagic Design/" + f"{programdata}/Blackmagic Design/" "DaVinci Resolve/Support/Developer/Scripting" ), "darwin": ( @@ -28,11 +29,10 @@ class ResolvePrelaunch(PreLaunchHook): ), "linux": "/opt/resolve/Developer/Scripting" } - RESOLVE_SCRIPT_API = os.path.normpath( - RESOLVE_SCRIPT_API_[current_platform]) - self.launch_context.env["RESOLVE_SCRIPT_API"] = RESOLVE_SCRIPT_API + resolve_script_api = Path(resolve_script_api_locations[current_platform]) + self.launch_context.env["RESOLVE_SCRIPT_API"] = resolve_script_api.as_posix() - RESOLVE_SCRIPT_LIB_ = { + resolve_script_lib_dirs = { "windows": ( "C:/Program Files/Blackmagic Design" "/DaVinci Resolve/fusionscript.dll" @@ -43,45 +43,39 @@ class ResolvePrelaunch(PreLaunchHook): ), "linux": "/opt/resolve/libs/Fusion/fusionscript.so" } - RESOLVE_SCRIPT_LIB = os.path.normpath( - RESOLVE_SCRIPT_LIB_[current_platform]) - self.launch_context.env["RESOLVE_SCRIPT_LIB"] = RESOLVE_SCRIPT_LIB + resolve_script_lib = Path(resolve_script_lib_dirs[current_platform]) + self.launch_context.env["RESOLVE_SCRIPT_LIB"] = resolve_script_lib.as_posix() - # TODO: add OTIO installation from `openpype/requirements.py` + # TODO: add OTIO installation from `openpype/requirements.py` # making sure python <3.9.* is installed at provided path - python3_home = os.path.normpath( - self.launch_context.env.get("RESOLVE_PYTHON3_HOME", "")) + python3_home = Path(self.launch_context.env.get("RESOLVE_PYTHON3_HOME", "")) - assert os.path.isdir(python3_home), ( + assert python3_home.is_dir(), ( "Python 3 is not installed at the provided folder path. Either " "make sure the `environments\resolve.json` is having correctly " "set `RESOLVE_PYTHON3_HOME` or make sure Python 3 is installed " f"in given path. \nRESOLVE_PYTHON3_HOME: `{python3_home}`" ) - self.launch_context.env["PYTHONHOME"] = python3_home - self.log.info(f"Path to Resolve Python folder: `{python3_home}`...") + python3_home_str = python3_home.as_posix() + self.launch_context.env["PYTHONHOME"] = python3_home_str + self.log.info(f"Path to Resolve Python folder: `{python3_home_str}`") - # add to the python path to path + # add to the python path to PATH env_path = self.launch_context.env["PATH"] - self.launch_context.env["PATH"] = os.pathsep.join([ - python3_home, - os.path.join(python3_home, "Scripts") - ] + env_path.split(os.pathsep)) + self.launch_context.env["PATH"] = f"{python3_home_str}{os.pathsep}{env_path}" self.log.debug(f"PATH: {self.launch_context.env['PATH']}") # add to the PYTHONPATH env_pythonpath = self.launch_context.env["PYTHONPATH"] - self.launch_context.env["PYTHONPATH"] = os.pathsep.join([ - os.path.join(python3_home, "Lib", "site-packages"), - os.path.join(RESOLVE_SCRIPT_API, "Modules"), - ] + env_pythonpath.split(os.pathsep)) + modules_path = Path(resolve_script_api, "Modules").as_posix() + self.launch_context.env["PYTHONPATH"] = f"{modules_path}{os.pathsep}{env_pythonpath}" self.log.debug(f"PYTHONPATH: {self.launch_context.env['PYTHONPATH']}") - RESOLVE_UTILITY_SCRIPTS_DIR_ = { + resolve_utility_scripts_dirs = { "windows": ( - f"{PROGRAMDATA}/Blackmagic Design" + f"{programdata}/Blackmagic Design" "/DaVinci Resolve/Fusion/Scripts/Comp" ), "darwin": ( @@ -90,12 +84,9 @@ class ResolvePrelaunch(PreLaunchHook): ), "linux": "/opt/resolve/Fusion/Scripts/Comp" } - RESOLVE_UTILITY_SCRIPTS_DIR = os.path.normpath( - RESOLVE_UTILITY_SCRIPTS_DIR_[current_platform] - ) + resolve_utility_scripts_dir = Path(resolve_utility_scripts_dirs[current_platform]) # setting utility scripts dir for scripts syncing - self.launch_context.env["RESOLVE_UTILITY_SCRIPTS_DIR"] = ( - RESOLVE_UTILITY_SCRIPTS_DIR) + self.launch_context.env["RESOLVE_UTILITY_SCRIPTS_DIR"] = resolve_utility_scripts_dir.as_posix() # remove terminal coloring tags self.launch_context.env["OPENPYPE_LOG_NO_COLORS"] = "True" From affc00c77c1bcf473a5aa13e40ef229eb2f30c0f Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov Date: Fri, 28 Apr 2023 17:59:26 +0300 Subject: [PATCH 007/189] remove bin folder from default values --- openpype/settings/defaults/system_settings/applications.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index df5b5e07c6..2c38676126 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -1069,8 +1069,8 @@ "RESOLVE_UTILITY_SCRIPTS_SOURCE_DIR": [], "RESOLVE_PYTHON3_HOME": { "windows": "{LOCALAPPDATA}/Programs/Python/Python36", - "darwin": "~/Library/Python/3.6/bin", - "linux": "/opt/Python/3.6/bin" + "darwin": "~/Library/Python/3.9", + "linux": "/opt/Python/3.9" } }, "variants": { From 74c1e6f3bb860c5aea18a669cb285d5fb2f0ca43 Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov Date: Fri, 28 Apr 2023 18:12:26 +0300 Subject: [PATCH 008/189] defaults to py3.6, set actual macos python path --- openpype/settings/defaults/system_settings/applications.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index 2c38676126..9aa30093cc 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -1069,8 +1069,8 @@ "RESOLVE_UTILITY_SCRIPTS_SOURCE_DIR": [], "RESOLVE_PYTHON3_HOME": { "windows": "{LOCALAPPDATA}/Programs/Python/Python36", - "darwin": "~/Library/Python/3.9", - "linux": "/opt/Python/3.9" + "darwin": "/Library/Frameworks/Python.framework/Versions/3.6", + "linux": "/opt/Python/3.6" } }, "variants": { From 4388da15df4b6a5f5a07e3514c54652e12b58691 Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov Date: Fri, 28 Apr 2023 18:32:33 +0300 Subject: [PATCH 009/189] black formatting --- .../hosts/resolve/hooks/pre_resolve_setup.py | 39 +++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/resolve/hooks/pre_resolve_setup.py b/openpype/hosts/resolve/hooks/pre_resolve_setup.py index 3144a60312..8c88478104 100644 --- a/openpype/hosts/resolve/hooks/pre_resolve_setup.py +++ b/openpype/hosts/resolve/hooks/pre_resolve_setup.py @@ -12,6 +12,7 @@ class ResolvePrelaunch(PreLaunchHook): path to the project by environment variable to Premiere launcher shell script. """ + app_groups = ["resolve"] def execute(self): @@ -27,10 +28,14 @@ class ResolvePrelaunch(PreLaunchHook): "/Library/Application Support/Blackmagic Design" "/DaVinci Resolve/Developer/Scripting" ), - "linux": "/opt/resolve/Developer/Scripting" + "linux": "/opt/resolve/Developer/Scripting", } - resolve_script_api = Path(resolve_script_api_locations[current_platform]) - self.launch_context.env["RESOLVE_SCRIPT_API"] = resolve_script_api.as_posix() + resolve_script_api = Path( + resolve_script_api_locations[current_platform] + ) + self.launch_context.env[ + "RESOLVE_SCRIPT_API" + ] = resolve_script_api.as_posix() resolve_script_lib_dirs = { "windows": ( @@ -41,14 +46,18 @@ class ResolvePrelaunch(PreLaunchHook): "/Applications/DaVinci Resolve/DaVinci Resolve.app" "/Contents/Libraries/Fusion/fusionscript.so" ), - "linux": "/opt/resolve/libs/Fusion/fusionscript.so" + "linux": "/opt/resolve/libs/Fusion/fusionscript.so", } resolve_script_lib = Path(resolve_script_lib_dirs[current_platform]) - self.launch_context.env["RESOLVE_SCRIPT_LIB"] = resolve_script_lib.as_posix() + self.launch_context.env[ + "RESOLVE_SCRIPT_LIB" + ] = resolve_script_lib.as_posix() # TODO: add OTIO installation from `openpype/requirements.py` # making sure python <3.9.* is installed at provided path - python3_home = Path(self.launch_context.env.get("RESOLVE_PYTHON3_HOME", "")) + python3_home = Path( + self.launch_context.env.get("RESOLVE_PYTHON3_HOME", "") + ) assert python3_home.is_dir(), ( "Python 3 is not installed at the provided folder path. Either " @@ -62,14 +71,18 @@ class ResolvePrelaunch(PreLaunchHook): # add to the python path to PATH env_path = self.launch_context.env["PATH"] - self.launch_context.env["PATH"] = f"{python3_home_str}{os.pathsep}{env_path}" + self.launch_context.env[ + "PATH" + ] = f"{python3_home_str}{os.pathsep}{env_path}" self.log.debug(f"PATH: {self.launch_context.env['PATH']}") # add to the PYTHONPATH env_pythonpath = self.launch_context.env["PYTHONPATH"] modules_path = Path(resolve_script_api, "Modules").as_posix() - self.launch_context.env["PYTHONPATH"] = f"{modules_path}{os.pathsep}{env_pythonpath}" + self.launch_context.env[ + "PYTHONPATH" + ] = f"{modules_path}{os.pathsep}{env_pythonpath}" self.log.debug(f"PYTHONPATH: {self.launch_context.env['PYTHONPATH']}") @@ -82,11 +95,15 @@ class ResolvePrelaunch(PreLaunchHook): "/Library/Application Support/Blackmagic Design" "/DaVinci Resolve/Fusion/Scripts/Comp" ), - "linux": "/opt/resolve/Fusion/Scripts/Comp" + "linux": "/opt/resolve/Fusion/Scripts/Comp", } - resolve_utility_scripts_dir = Path(resolve_utility_scripts_dirs[current_platform]) + resolve_utility_scripts_dir = Path( + resolve_utility_scripts_dirs[current_platform] + ) # setting utility scripts dir for scripts syncing - self.launch_context.env["RESOLVE_UTILITY_SCRIPTS_DIR"] = resolve_utility_scripts_dir.as_posix() + self.launch_context.env[ + "RESOLVE_UTILITY_SCRIPTS_DIR" + ] = resolve_utility_scripts_dir.as_posix() # remove terminal coloring tags self.launch_context.env["OPENPYPE_LOG_NO_COLORS"] = "True" From 1d2123dce8cf3d5fd70467cac32e1f9a484ee199 Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov <11698866+movalex@users.noreply.github.com> Date: Sat, 29 Apr 2023 10:42:15 +0300 Subject: [PATCH 010/189] Update .gitignore Co-authored-by: Roy Nieterau --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index a565c57b54..18e7cd7bf2 100644 --- a/.gitignore +++ b/.gitignore @@ -85,7 +85,6 @@ openpype/premiere/ppro/js/debug.log .env dump.sql test_localsystem.txt -*.code-workspace # website ########## From 84cef9d3cf6135fd26b490f57e9bc68d9da36ace Mon Sep 17 00:00:00 2001 From: Michael reda Date: Tue, 2 May 2023 11:12:18 +0200 Subject: [PATCH 011/189] handel long lines --- openpype/modules/kitsu/kitsu_module.py | 19 ++++++-- .../modules/kitsu/utils/update_op_with_zou.py | 48 +++++++++---------- 2 files changed, 40 insertions(+), 27 deletions(-) diff --git a/openpype/modules/kitsu/kitsu_module.py b/openpype/modules/kitsu/kitsu_module.py index f4e3dd5691..7c9d888aa7 100644 --- a/openpype/modules/kitsu/kitsu_module.py +++ b/openpype/modules/kitsu/kitsu_module.py @@ -124,10 +124,23 @@ def push_to_zou(login, password): @cli_main.command() -@click.option("-prjs", "--projects", envvar="SYNC_PROJECTS", help="Sync specific kitsu projects") -@click.option("-l", "--login", envvar="KITSU_LOGIN", help="Kitsu login") @click.option( - "-p", "--password", envvar="KITSU_PWD", help="Password for kitsu username" + "-prjs", + "--projects", + envvar="SYNC_PROJECTS", + help="Sync specific kitsu projects" +) +@click.option( + "-l", + "--login", + envvar="KITSU_LOGIN", + help="Kitsu login" +) +@click.option( + "-p", + "--password", + envvar="KITSU_PWD", + help="Password for kitsu username" ) def sync_service(login, password, projects="^"): """Synchronize openpype database from Zou sever database. diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index a397198a13..ad8ccd9f3f 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -63,11 +63,11 @@ def set_op_project(dbcon: AvalonMongoDB, project_id: str): def update_op_assets( - dbcon: AvalonMongoDB, - gazu_project: dict, - project_doc: dict, - entities_list: List[dict], - asset_doc_ids: Dict[str, dict], + dbcon: AvalonMongoDB, + gazu_project: dict, + project_doc: dict, + entities_list: List[dict], + asset_doc_ids: Dict[str, dict], ) -> List[Dict[str, dict]]: """Update OpenPype assets. Set 'data' and 'parent' fields. @@ -210,10 +210,10 @@ def update_op_assets( item.get("entity_type_id") if item_type == "Asset" else None - # Else, fallback on usual hierarchy - or item.get("parent_id") - or item.get("episode_id") - or item.get("source_id") + # Else, fallback on usual hierarchy + or item.get("parent_id") + or item.get("episode_id") + or item.get("source_id") ) # Substitute item type for general classification (assets or shots) @@ -350,7 +350,7 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: "config.tasks": { t["name"]: {"short_name": t.get("short_name", t["name"])} for t in gazu.task.all_task_types_for_project(project) - or gazu.task.all_task_types() + or gazu.task.all_task_types() }, "data": project_data, } @@ -359,7 +359,8 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: def sync_all_projects( - login: str, password: str, ignore_projects: list = None, specific_projects: list = None + login: str, password: str, ignore_projects: list = None, + specific_projects: list = None ): """Update all OP projects in DB with Zou data. @@ -383,7 +384,6 @@ def sync_all_projects( dbcon.install() all_projects = gazu.project.all_projects() - project_to_sync = [] if specific_projects == ['*']: project_to_sync = all_projects @@ -435,8 +435,8 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): # Do not sync closed kitsu project that is not found in openpype if ( - project['project_status_name'] == "Closed" - and not get_project(project['name']) + project['project_status_name'] == "Closed" + and not get_project(project['name']) ): return @@ -451,10 +451,10 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): all_entities = [ item for item in all_assets - + all_asset_types - + all_episodes - + all_seqs - + all_shots + + all_asset_types + + all_episodes + + all_seqs + + all_shots if naming_pattern.match(item["name"]) ] @@ -526,12 +526,12 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): [ UpdateOne({"_id": id}, update) for id, update in update_op_assets( - dbcon, - project, - project_dict, - all_entities, - zou_ids_and_asset_docs, - ) + dbcon, + project, + project_dict, + all_entities, + zou_ids_and_asset_docs, + ) ] ) From 2a9a7d6fd4f0a96dc70b1f61525d5b8f1ac05eb5 Mon Sep 17 00:00:00 2001 From: Michael <71185790+Michaelredaa@users.noreply.github.com> Date: Wed, 3 May 2023 12:18:43 +0300 Subject: [PATCH 012/189] handle reviews --- openpype/modules/kitsu/kitsu_module.py | 19 ++---- .../modules/kitsu/utils/update_op_with_zou.py | 62 +++++++++---------- 2 files changed, 36 insertions(+), 45 deletions(-) diff --git a/openpype/modules/kitsu/kitsu_module.py b/openpype/modules/kitsu/kitsu_module.py index 7c9d888aa7..2a3a0f3bff 100644 --- a/openpype/modules/kitsu/kitsu_module.py +++ b/openpype/modules/kitsu/kitsu_module.py @@ -125,9 +125,10 @@ def push_to_zou(login, password): @cli_main.command() @click.option( - "-prjs", - "--projects", - envvar="SYNC_PROJECTS", + "-prj", + "--project", + multiple=True, + default=[""] help="Sync specific kitsu projects" ) @click.option( @@ -142,26 +143,18 @@ def push_to_zou(login, password): envvar="KITSU_PWD", help="Password for kitsu username" ) -def sync_service(login, password, projects="^"): +def sync_service(login, password, project): """Synchronize openpype database from Zou sever database. Args: login (str): Kitsu user login password (str): Kitsu user password projects (str): specific kitsu projects - - SYNC_PROJECTS: - *: all projects - ^: dont sync any project just listen - "project01 project02 ...": to choose custom projects - - """ from .utils.update_op_with_zou import sync_all_projects from .utils.sync_service import start_listeners - projects = projects.strip() - projects = projects.split(' ') + projects = ' '.join(project) sync_all_projects(login, password, specific_projects=projects) start_listeners(login, password) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index ad8ccd9f3f..7983765e83 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -63,11 +63,11 @@ def set_op_project(dbcon: AvalonMongoDB, project_id: str): def update_op_assets( - dbcon: AvalonMongoDB, - gazu_project: dict, - project_doc: dict, - entities_list: List[dict], - asset_doc_ids: Dict[str, dict], + dbcon: AvalonMongoDB, + gazu_project: dict, + project_doc: dict, + entities_list: List[dict], + asset_doc_ids: Dict[str, dict], ) -> List[Dict[str, dict]]: """Update OpenPype assets. Set 'data' and 'parent' fields. @@ -210,10 +210,10 @@ def update_op_assets( item.get("entity_type_id") if item_type == "Asset" else None - # Else, fallback on usual hierarchy - or item.get("parent_id") - or item.get("episode_id") - or item.get("source_id") + # Else, fallback on usual hierarchy + or item.get("parent_id") + or item.get("episode_id") + or item.get("source_id") ) # Substitute item type for general classification (assets or shots) @@ -350,7 +350,7 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: "config.tasks": { t["name"]: {"short_name": t.get("short_name", t["name"])} for t in gazu.task.all_task_types_for_project(project) - or gazu.task.all_task_types() + or gazu.task.all_task_types() }, "data": project_data, } @@ -358,9 +358,8 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: ) -def sync_all_projects( - login: str, password: str, ignore_projects: list = None, - specific_projects: list = None +def sync_all_projects(login: str, password: str, ignore_projects: list = None, + filter_projects: list = None ): """Update all OP projects in DB with Zou data. @@ -368,7 +367,7 @@ def sync_all_projects( login (str): Kitsu user login password (str): Kitsu user password ignore_projects (list): List of unsynced project names - specific_projects (list): List of synced project names + filter_projects (list): List of filter project names to sync with Raises: gazu.exception.AuthFailedException: Wrong user login and/or password """ @@ -385,22 +384,21 @@ def sync_all_projects( all_projects = gazu.project.all_projects() project_to_sync = [] - if specific_projects == ['*']: + + if not filter_projects: + # listen only + return + + if filter_projects == ['*']: project_to_sync = all_projects - elif specific_projects == ['^']: - return - - elif isinstance(specific_projects, list): - all_kitsu_projects = {p['name']: p for p in all_projects} - for proj_name in specific_projects: - if proj_name in all_kitsu_projects: - project_to_sync.append(all_kitsu_projects[proj_name]) - else: - log.info(f'`{proj_name}` project does not exists in kitsu.' - f' Please make sure you write the project correctly.') - else: - return + all_kitsu_projects = {p['name']: p for p in all_projects} + for proj_name in filter_projects: + if proj_name in all_kitsu_projects: + project_to_sync.append(all_kitsu_projects[proj_name]) + else: + log.info(f'`{proj_name}` project does not exist in Kitsu.' + f' Please make sure the project is spelled correctly.') for project in project_to_sync: if ignore_projects and project["name"] in ignore_projects: @@ -451,10 +449,10 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): all_entities = [ item for item in all_assets - + all_asset_types - + all_episodes - + all_seqs - + all_shots + + all_asset_types + + all_episodes + + all_seqs + + all_shots if naming_pattern.match(item["name"]) ] From 0a3d206680ed7b21cfb30f1800ae1625e284793d Mon Sep 17 00:00:00 2001 From: Michael <71185790+Michaelredaa@users.noreply.github.com> Date: Wed, 3 May 2023 12:23:17 +0300 Subject: [PATCH 013/189] solve syntex error --- openpype/modules/kitsu/kitsu_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/kitsu/kitsu_module.py b/openpype/modules/kitsu/kitsu_module.py index 2a3a0f3bff..59ad2efd29 100644 --- a/openpype/modules/kitsu/kitsu_module.py +++ b/openpype/modules/kitsu/kitsu_module.py @@ -128,7 +128,7 @@ def push_to_zou(login, password): "-prj", "--project", multiple=True, - default=[""] + default=[""], help="Sync specific kitsu projects" ) @click.option( From 6e9b6f6bef0c290dd46b7b3669fad2d3b338c0e8 Mon Sep 17 00:00:00 2001 From: Michael <71185790+Michaelredaa@users.noreply.github.com> Date: Wed, 3 May 2023 12:38:42 +0300 Subject: [PATCH 014/189] correct arg name in sync_all_projects --- openpype/modules/kitsu/kitsu_module.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/modules/kitsu/kitsu_module.py b/openpype/modules/kitsu/kitsu_module.py index 59ad2efd29..bd8ade62d8 100644 --- a/openpype/modules/kitsu/kitsu_module.py +++ b/openpype/modules/kitsu/kitsu_module.py @@ -149,12 +149,10 @@ def sync_service(login, password, project): Args: login (str): Kitsu user login password (str): Kitsu user password - projects (str): specific kitsu projects + project (str): specific kitsu projects """ from .utils.update_op_with_zou import sync_all_projects from .utils.sync_service import start_listeners - projects = ' '.join(project) - - sync_all_projects(login, password, specific_projects=projects) + sync_all_projects(login, password, filter_projects=project) start_listeners(login, password) From ad4f5876ce18239b244d59332ed40b5469cc0433 Mon Sep 17 00:00:00 2001 From: Michael <71185790+Michaelredaa@users.noreply.github.com> Date: Wed, 3 May 2023 12:40:32 +0300 Subject: [PATCH 015/189] update condition to sync specific projects --- .../modules/kitsu/utils/update_op_with_zou.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index 7983765e83..40e4191508 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -389,16 +389,18 @@ def sync_all_projects(login: str, password: str, ignore_projects: list = None, # listen only return - if filter_projects == ['*']: + if '*' in filter_projects: + # all projects project_to_sync = all_projects - all_kitsu_projects = {p['name']: p for p in all_projects} - for proj_name in filter_projects: - if proj_name in all_kitsu_projects: - project_to_sync.append(all_kitsu_projects[proj_name]) - else: - log.info(f'`{proj_name}` project does not exist in Kitsu.' - f' Please make sure the project is spelled correctly.') + else: + all_kitsu_projects = {p['name']: p for p in all_projects} + for proj_name in filter_projects: + if proj_name in all_kitsu_projects: + project_to_sync.append(all_kitsu_projects[proj_name]) + else: + log.info(f'`{proj_name}` project does not exist in Kitsu.' + f' Please make sure the project is spelled correctly.') for project in project_to_sync: if ignore_projects and project["name"] in ignore_projects: From d6e7cd638d403f0d3f8ad53e7fef444122b1dc39 Mon Sep 17 00:00:00 2001 From: Michael <71185790+Michaelredaa@users.noreply.github.com> Date: Wed, 3 May 2023 13:02:08 +0300 Subject: [PATCH 016/189] update kitsu docs --- website/docs/module_kitsu.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/website/docs/module_kitsu.md b/website/docs/module_kitsu.md index d79c78fecf..4b827b3802 100644 --- a/website/docs/module_kitsu.md +++ b/website/docs/module_kitsu.md @@ -16,10 +16,18 @@ If you want to connect Kitsu to OpenPype you have to set the `Server` url in Kit This setting is available for all the users of the OpenPype instance. ## Synchronize -Updating OP with Kitsu data is executed running the `sync-service`, which requires to provide your Kitsu credentials with `-l, --login` and `-p, --password` or by setting the environment variables `KITSU_LOGIN` and `KITSU_PWD`. This process will request data from Kitsu and create/delete/update OP assets. +Updating OP with Kitsu data is executed running the `sync-service`, which requires to provide your Kitsu credentials with `-l, --login` and `-p, --password` or by setting the environment variables `KITSU_LOGIN` and `KITSU_PWD`. This process will request data from Kitsu projects with `-proj, --project` and create/delete/update OP assets. Once this sync is done, the thread will automatically start a loop to listen to Kitsu events. +The args for `-proj, --project` accept multiple project name, `-proj *` to sync all active projects, and the default value to start a loop to listen to Kitsu events only without any sync. ```bash +// sync specific projects then run listen +openpype_console module kitsu sync-service -l me@domain.ext -p my_password -proj project_name01 -proj project_name02 + +// sync all projects then run listen +openpype_console module kitsu sync-service -l me@domain.ext -p my_password -proj * + +// start listen only openpype_console module kitsu sync-service -l me@domain.ext -p my_password ``` From 925ea3fe9312a3e78df1604e370a019cb2adbe5c Mon Sep 17 00:00:00 2001 From: Michael <71185790+Michaelredaa@users.noreply.github.com> Date: Wed, 3 May 2023 20:14:33 +0300 Subject: [PATCH 017/189] Update module_kitsu docs --- website/docs/module_kitsu.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/website/docs/module_kitsu.md b/website/docs/module_kitsu.md index 4b827b3802..970bfb275e 100644 --- a/website/docs/module_kitsu.md +++ b/website/docs/module_kitsu.md @@ -16,19 +16,22 @@ If you want to connect Kitsu to OpenPype you have to set the `Server` url in Kit This setting is available for all the users of the OpenPype instance. ## Synchronize -Updating OP with Kitsu data is executed running the `sync-service`, which requires to provide your Kitsu credentials with `-l, --login` and `-p, --password` or by setting the environment variables `KITSU_LOGIN` and `KITSU_PWD`. This process will request data from Kitsu projects with `-proj, --project` and create/delete/update OP assets. +Updating OP with Kitsu data is executed running the `sync-service`, which requires to provide your Kitsu credentials with `-l, --login` and `-p, --password` or by setting the environment variables `KITSU_LOGIN` and `KITSU_PWD`. This process will request data from Kitsu and create/delete/update OP assets. Once this sync is done, the thread will automatically start a loop to listen to Kitsu events. -The args for `-proj, --project` accept multiple project name, `-proj *` to sync all active projects, and the default value to start a loop to listen to Kitsu events only without any sync. +- `-prj, --project` This flag accepts multiple project name to sync specific projects, and the default to sync all projects. +- `-lo, --listen-only` This flag to run listen to Kitsu events only without any sync. + +Note: You must use one argument of `-pro` or `-lo`, because the listen only flag override syncing flag. ```bash -// sync specific projects then run listen -openpype_console module kitsu sync-service -l me@domain.ext -p my_password -proj project_name01 -proj project_name02 - // sync all projects then run listen -openpype_console module kitsu sync-service -l me@domain.ext -p my_password -proj * +openpype_console module kitsu sync-service -l me@domain.ext -p my_password + +// sync specific projects then run listen +openpype_console module kitsu sync-service -l me@domain.ext -p my_password -prj project_name01 -prj project_name02 // start listen only -openpype_console module kitsu sync-service -l me@domain.ext -p my_password +openpype_console module kitsu sync-service -l me@domain.ext -p my_password -lo ``` ### Events listening From c5dd4d21eed2b7d9b67ecdb980a291742c050e7c Mon Sep 17 00:00:00 2001 From: Michael <71185790+Michaelredaa@users.noreply.github.com> Date: Wed, 3 May 2023 20:16:23 +0300 Subject: [PATCH 018/189] Add listen-only flag to sync --- openpype/modules/kitsu/kitsu_module.py | 27 +++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/openpype/modules/kitsu/kitsu_module.py b/openpype/modules/kitsu/kitsu_module.py index bd8ade62d8..319b5de16b 100644 --- a/openpype/modules/kitsu/kitsu_module.py +++ b/openpype/modules/kitsu/kitsu_module.py @@ -124,13 +124,6 @@ def push_to_zou(login, password): @cli_main.command() -@click.option( - "-prj", - "--project", - multiple=True, - default=[""], - help="Sync specific kitsu projects" -) @click.option( "-l", "--login", @@ -143,16 +136,32 @@ def push_to_zou(login, password): envvar="KITSU_PWD", help="Password for kitsu username" ) -def sync_service(login, password, project): +@click.option( + "-prj", + "--project", + multiple=True, + default=[], + help="Sync specific kitsu projects" +) +@click.option( + "-lo", + "--listen_only/--listen-only", + default=False, + help="Listen to events only without any syncing" +) +def sync_service(login, password, project, listen_only): """Synchronize openpype database from Zou sever database. Args: login (str): Kitsu user login password (str): Kitsu user password project (str): specific kitsu projects + listen_only (bool): run listen only without any syncing """ from .utils.update_op_with_zou import sync_all_projects from .utils.sync_service import start_listeners - sync_all_projects(login, password, filter_projects=project) + if not listen_only: + sync_all_projects(login, password, filter_projects=project) + start_listeners(login, password) From 44e533ff3306aa218a25960a1a41062e3fa82b5a Mon Sep 17 00:00:00 2001 From: Michael <71185790+Michaelredaa@users.noreply.github.com> Date: Wed, 3 May 2023 20:20:43 +0300 Subject: [PATCH 019/189] Update sync_all_projects function with filtered projects --- .../modules/kitsu/utils/update_op_with_zou.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index 40e4191508..bfb4bd58fa 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -367,7 +367,7 @@ def sync_all_projects(login: str, password: str, ignore_projects: list = None, login (str): Kitsu user login password (str): Kitsu user password ignore_projects (list): List of unsynced project names - filter_projects (list): List of filter project names to sync with + filter_projects (tuple): Tuple of filter project names to sync with Raises: gazu.exception.AuthFailedException: Wrong user login and/or password """ @@ -385,15 +385,7 @@ def sync_all_projects(login: str, password: str, ignore_projects: list = None, project_to_sync = [] - if not filter_projects: - # listen only - return - - if '*' in filter_projects: - # all projects - project_to_sync = all_projects - - else: + if filter_projects: all_kitsu_projects = {p['name']: p for p in all_projects} for proj_name in filter_projects: if proj_name in all_kitsu_projects: @@ -401,6 +393,9 @@ def sync_all_projects(login: str, password: str, ignore_projects: list = None, else: log.info(f'`{proj_name}` project does not exist in Kitsu.' f' Please make sure the project is spelled correctly.') + else: + # all project + project_to_sync = all_projects for project in project_to_sync: if ignore_projects and project["name"] in ignore_projects: From b9055d61af83760430cce647f7f2226b23b91bcf Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 4 May 2023 17:42:16 +0200 Subject: [PATCH 020/189] POC wip --- .../fusion/plugins/create/create_saver.py | 2 +- .../fusion/plugins/publish/collect_renders.py | 8 + .../plugins/publish/submit_fusion_deadline.py | 140 ++++++++++++++++-- 3 files changed, 133 insertions(+), 17 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index cedc4029fa..a66d9b7e86 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -28,7 +28,7 @@ class CreateSaver(Creator): description = "Fusion Saver to generate image sequence" icon = "fa5.eye" - instance_attributes = ["reviewable"] + instance_attributes = ["reviewable", "farm_rendering"] def create(self, subset_name, instance_data, pre_create_data): # TODO: Add pre_create attributes to choose file format? diff --git a/openpype/hosts/fusion/plugins/publish/collect_renders.py b/openpype/hosts/fusion/plugins/publish/collect_renders.py index 7f38e68447..b1c12c7393 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_renders.py +++ b/openpype/hosts/fusion/plugins/publish/collect_renders.py @@ -23,3 +23,11 @@ class CollectFusionRenders(pyblish.api.InstancePlugin): instance.data["families"].append( "{}.{}".format(family, render_target) ) + if render_target == "farm": + if "review" in instance.data["families"]: + instance.data["families"].remove("review") + + # Farm rendering + instance.data["transfer"] = False + instance.data["farm"] = True + self.log.info("Farm rendering ON ...") diff --git a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py index 8570c759bc..2885d91d07 100644 --- a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py @@ -1,15 +1,26 @@ import os import json import getpass +from pprint import pformat import requests import pyblish.api from openpype.pipeline import legacy_io +from openpype.pipeline.publish import ( + OpenPypePyblishPluginMixin +) +from openpype.lib import ( + BoolDef, + NumberDef +) -class FusionSubmitDeadline(pyblish.api.InstancePlugin): +class FusionSubmitDeadline( + pyblish.api.InstancePlugin, + OpenPypePyblishPluginMixin +): """Submit current Comp to Deadline Renders are submitted to a Deadline Web Service as @@ -17,12 +28,76 @@ class FusionSubmitDeadline(pyblish.api.InstancePlugin): """ - label = "Submit to Deadline" + label = "Submit Fusion to Deadline" order = pyblish.api.IntegratorOrder hosts = ["fusion"] - families = ["render.farm"] + families = ["render"] + targets = ["local"] + + # presets + priority = 50 + chunk_size = 1 + concurrent_tasks = 1 + group = "" + department = "" + limit_groups = {} + use_gpu = False + env_allowed_keys = [] + env_search_replace_values = {} + + @classmethod + def get_attribute_defs(cls): + return [ + NumberDef( + "priority", + label="Priority", + default=cls.priority, + decimals=0 + ), + NumberDef( + "chunk", + label="Frames Per Task", + default=cls.chunk_size, + decimals=0, + minimum=1, + maximum=1000 + ), + NumberDef( + "concurrency", + label="Concurrency", + default=cls.concurrent_tasks, + decimals=0, + minimum=1, + maximum=10 + ), + BoolDef( + "use_gpu", + default=cls.use_gpu, + label="Use GPU" + ), + BoolDef( + "suspend_publish", + default=False, + label="Suspend publish" + ) + ] def process(self, instance): + if not instance.data.get("farm"): + self.log.info("Skipping local instance.") + return + + attribute_values = self.get_attr_values_from_data( + instance.data) + + self.log.debug(pformat(attribute_values)) + + # add suspend_publish attributeValue to instance data + instance.data["suspend_publish"] = attribute_values[ + "suspend_publish"] + + instance.data["toBeRenderedOn"] = "deadline" + context = instance.context key = "__hasRun{}".format(self.__class__.__name__) @@ -33,24 +108,24 @@ class FusionSubmitDeadline(pyblish.api.InstancePlugin): from openpype.hosts.fusion.api.lib import get_frame_path - deadline_url = ( - context.data["system_settings"] - ["modules"] - ["deadline"] - ["DEADLINE_REST_URL"] - ) - assert deadline_url, "Requires DEADLINE_REST_URL" + # get default deadline webservice url from deadline module + deadline_url = instance.context.data["defaultDeadline"] + # if custom one is set in instance, use that + if instance.data.get("deadlineUrl"): + deadline_url = instance.data.get("deadlineUrl") + assert deadline_url, "Requires Deadline Webservice URL" # Collect all saver instances in context that are to be rendered saver_instances = [] - for instance in context[:]: - if not self.families[0] in instance.data.get("families"): + for instance in context: + if instance.data["family"] != "render": # Allow only saver family instances continue if not instance.data.get("publish", True): # Skip inactive instances continue + self.log.debug(instance.data["name"]) saver_instances.append(instance) @@ -58,11 +133,31 @@ class FusionSubmitDeadline(pyblish.api.InstancePlugin): raise RuntimeError("No instances found for Deadline submittion") fusion_version = int(context.data["fusionVersion"]) - filepath = context.data["currentFile"] - filename = os.path.basename(filepath) comment = context.data.get("comment", "") deadline_user = context.data.get("deadlineUser", getpass.getuser()) + script_path = context.data["currentFile"] + + for item in context: + if "workfile" in item.data["families"]: + msg = "Workfile (scene) must be published along" + assert item.data["publish"] is True, msg + + template_data = item.data.get("anatomyData") + rep = item.data.get("representations")[0].get("name") + template_data["representation"] = rep + template_data["ext"] = rep + template_data["comment"] = None + anatomy_filled = context.data["anatomy"].format(template_data) + template_filled = anatomy_filled["publish"]["path"] + script_path = os.path.normpath(template_filled) + + self.log.info( + "Using published scene for render {}".format(script_path) + ) + + filename = os.path.basename(script_path) + # Documentation for keys available at: # https://docs.thinkboxsoftware.com # /products/deadline/8.0/1_User%20Manual/manual @@ -73,11 +168,20 @@ class FusionSubmitDeadline(pyblish.api.InstancePlugin): "BatchName": filename, # Asset dependency to wait for at least the scene file to sync. - "AssetDependency0": filepath, + "AssetDependency0": script_path, # Job name, as seen in Monitor "Name": filename, + "Priority": attribute_values.get( + "priority", self.priority), + "ChunkSize": attribute_values.get( + "chunk", self.chunk_size), + "ConcurrentTasks": attribute_values.get( + "concurrency", + self.concurrent_tasks + ), + # User, as seen in Monitor "UserName": deadline_user, @@ -94,7 +198,7 @@ class FusionSubmitDeadline(pyblish.api.InstancePlugin): }, "PluginInfo": { # Input - "FlowFile": filepath, + "FlowFile": script_path, # Mandatory for Deadline "Version": str(fusion_version), @@ -109,6 +213,10 @@ class FusionSubmitDeadline(pyblish.api.InstancePlugin): # Proxy: higher numbers smaller images for faster test renders # 1 = no proxy quality "Proxy": 1, + + # using GPU by default + "UseGpu": attribute_values.get( + "use_gpu", self.use_gpu) }, # Mandatory for Deadline, may be empty From 8242e61ad893843f4539e6a1517e50aa4e785de2 Mon Sep 17 00:00:00 2001 From: Michael reda Date: Fri, 5 May 2023 13:54:24 +0200 Subject: [PATCH 021/189] update linting --- openpype/modules/kitsu/kitsu_module.py | 18 ++----- .../modules/kitsu/utils/update_op_with_zou.py | 54 ++++++++++--------- 2 files changed, 33 insertions(+), 39 deletions(-) diff --git a/openpype/modules/kitsu/kitsu_module.py b/openpype/modules/kitsu/kitsu_module.py index 319b5de16b..dec19989ea 100644 --- a/openpype/modules/kitsu/kitsu_module.py +++ b/openpype/modules/kitsu/kitsu_module.py @@ -94,7 +94,7 @@ class KitsuModule(OpenPypeModule, IPluginPaths, ITrayAction): return { "publish": [os.path.join(current_dir, "plugins", "publish")], - "actions": [os.path.join(current_dir, "actions")] + "actions": [os.path.join(current_dir, "actions")], } def cli(self, click_group): @@ -124,30 +124,22 @@ def push_to_zou(login, password): @cli_main.command() +@click.option("-l", "--login", envvar="KITSU_LOGIN", help="Kitsu login") @click.option( - "-l", - "--login", - envvar="KITSU_LOGIN", - help="Kitsu login" -) -@click.option( - "-p", - "--password", - envvar="KITSU_PWD", - help="Password for kitsu username" + "-p", "--password", envvar="KITSU_PWD", help="Password for kitsu username" ) @click.option( "-prj", "--project", multiple=True, default=[], - help="Sync specific kitsu projects" + help="Sync specific kitsu projects", ) @click.option( "-lo", "--listen_only/--listen-only", default=False, - help="Listen to events only without any syncing" + help="Listen to events only without any syncing", ) def sync_service(login, password, project, listen_only): """Synchronize openpype database from Zou sever database. diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index bfb4bd58fa..4ad08bb739 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -94,9 +94,7 @@ def update_op_assets( if not item_doc: # Create asset op_asset = create_op_asset(item) insert_result = dbcon.insert_one(op_asset) - item_doc = get_asset_by_id( - project_name, insert_result.inserted_id - ) + item_doc = get_asset_by_id(project_name, insert_result.inserted_id) # Update asset item_data = deepcopy(item_doc["data"]) @@ -210,10 +208,10 @@ def update_op_assets( item.get("entity_type_id") if item_type == "Asset" else None - # Else, fallback on usual hierarchy - or item.get("parent_id") - or item.get("episode_id") - or item.get("source_id") + # Else, fallback on usual hierarchy + or item.get("parent_id") + or item.get("episode_id") + or item.get("source_id") ) # Substitute item type for general classification (assets or shots) @@ -329,7 +327,7 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: "code": project_code, "fps": float(project["fps"]), "zou_id": project["id"], - "active": project['project_status_name'] != "Closed", + "active": project["project_status_name"] != "Closed", } ) @@ -350,7 +348,7 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: "config.tasks": { t["name"]: {"short_name": t.get("short_name", t["name"])} for t in gazu.task.all_task_types_for_project(project) - or gazu.task.all_task_types() + or gazu.task.all_task_types() }, "data": project_data, } @@ -358,8 +356,11 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: ) -def sync_all_projects(login: str, password: str, ignore_projects: list = None, - filter_projects: list = None +def sync_all_projects( + login: str, + password: str, + ignore_projects: list = None, + filter_projects: list = None, ): """Update all OP projects in DB with Zou data. @@ -386,13 +387,15 @@ def sync_all_projects(login: str, password: str, ignore_projects: list = None, project_to_sync = [] if filter_projects: - all_kitsu_projects = {p['name']: p for p in all_projects} + all_kitsu_projects = {p["name"]: p for p in all_projects} for proj_name in filter_projects: if proj_name in all_kitsu_projects: project_to_sync.append(all_kitsu_projects[proj_name]) else: - log.info(f'`{proj_name}` project does not exist in Kitsu.' - f' Please make sure the project is spelled correctly.') + log.info( + f"`{proj_name}` project does not exist in Kitsu." + f" Please make sure the project is spelled correctly." + ) else: # all project project_to_sync = all_projects @@ -424,14 +427,13 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): # Get all statuses for projects from Kitsu all_status = gazu.project.all_project_status() for status in all_status: - if project['project_status_id'] == status['id']: - project['project_status_name'] = status['name'] + if project["project_status_id"] == status["id"]: + project["project_status_name"] = status["name"] break # Do not sync closed kitsu project that is not found in openpype - if ( - project['project_status_name'] == "Closed" - and not get_project(project['name']) + if project["project_status_name"] == "Closed" and not get_project( + project["name"] ): return @@ -460,7 +462,7 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): log.info("Project created: {}".format(project_name)) bulk_writes.append(write_project_to_op(project, dbcon)) - if project['project_status_name'] == "Closed": + if project["project_status_name"] == "Closed": return # Try to find project document @@ -521,12 +523,12 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): [ UpdateOne({"_id": id}, update) for id, update in update_op_assets( - dbcon, - project, - project_dict, - all_entities, - zou_ids_and_asset_docs, - ) + dbcon, + project, + project_dict, + all_entities, + zou_ids_and_asset_docs, + ) ] ) From 07cf84e6481d013e79493e3d87c0189ab9ecf2cd Mon Sep 17 00:00:00 2001 From: Michael reda Date: Fri, 5 May 2023 14:14:12 +0200 Subject: [PATCH 022/189] add variable name to `@click.option` --- openpype/modules/kitsu/kitsu_module.py | 11 +++++++---- openpype/modules/kitsu/utils/update_op_with_zou.py | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/openpype/modules/kitsu/kitsu_module.py b/openpype/modules/kitsu/kitsu_module.py index dec19989ea..8d2d5ccd60 100644 --- a/openpype/modules/kitsu/kitsu_module.py +++ b/openpype/modules/kitsu/kitsu_module.py @@ -131,29 +131,32 @@ def push_to_zou(login, password): @click.option( "-prj", "--project", + "projects", multiple=True, default=[], help="Sync specific kitsu projects", ) @click.option( "-lo", - "--listen_only/--listen-only", + "--listen-only", + "listen_only", + is_flag=True, default=False, help="Listen to events only without any syncing", ) -def sync_service(login, password, project, listen_only): +def sync_service(login, password, projects, listen_only): """Synchronize openpype database from Zou sever database. Args: login (str): Kitsu user login password (str): Kitsu user password - project (str): specific kitsu projects + projects (tuple): specific kitsu projects listen_only (bool): run listen only without any syncing """ from .utils.update_op_with_zou import sync_all_projects from .utils.sync_service import start_listeners if not listen_only: - sync_all_projects(login, password, filter_projects=project) + sync_all_projects(login, password, filter_projects=projects) start_listeners(login, password) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index 4ad08bb739..b495cd1bea 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -360,7 +360,7 @@ def sync_all_projects( login: str, password: str, ignore_projects: list = None, - filter_projects: list = None, + filter_projects: tuple = None, ): """Update all OP projects in DB with Zou data. From a5efddf96926cd2a3137dc9a5ef6e7fd272d3326 Mon Sep 17 00:00:00 2001 From: Michael <71185790+Michaelredaa@users.noreply.github.com> Date: Fri, 5 May 2023 15:47:39 +0300 Subject: [PATCH 023/189] Update docs to be more specific MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: FΓ©lix David --- website/docs/module_kitsu.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/module_kitsu.md b/website/docs/module_kitsu.md index 970bfb275e..9695542723 100644 --- a/website/docs/module_kitsu.md +++ b/website/docs/module_kitsu.md @@ -30,7 +30,7 @@ openpype_console module kitsu sync-service -l me@domain.ext -p my_password // sync specific projects then run listen openpype_console module kitsu sync-service -l me@domain.ext -p my_password -prj project_name01 -prj project_name02 -// start listen only +// start listen only for all projects openpype_console module kitsu sync-service -l me@domain.ext -p my_password -lo ``` From 1e26b177261a0dfe9ce5baf43ba43ecec22eb735 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 9 May 2023 17:20:36 +0200 Subject: [PATCH 024/189] Fusion: rewrite collecting renders --- .../publish/collect_expected_frames.py | 50 ----- .../plugins/publish/collect_fusion_version.py | 22 -- .../plugins/publish/collect_instances.py | 28 --- .../fusion/plugins/publish/collect_render.py | 206 ++++++++++++++++++ .../fusion/plugins/publish/collect_renders.py | 33 --- 5 files changed, 206 insertions(+), 133 deletions(-) delete mode 100644 openpype/hosts/fusion/plugins/publish/collect_expected_frames.py delete mode 100644 openpype/hosts/fusion/plugins/publish/collect_fusion_version.py create mode 100644 openpype/hosts/fusion/plugins/publish/collect_render.py delete mode 100644 openpype/hosts/fusion/plugins/publish/collect_renders.py diff --git a/openpype/hosts/fusion/plugins/publish/collect_expected_frames.py b/openpype/hosts/fusion/plugins/publish/collect_expected_frames.py deleted file mode 100644 index 0ba777629f..0000000000 --- a/openpype/hosts/fusion/plugins/publish/collect_expected_frames.py +++ /dev/null @@ -1,50 +0,0 @@ -import pyblish.api -from openpype.pipeline import publish -import os - - -class CollectFusionExpectedFrames( - pyblish.api.InstancePlugin, publish.ColormanagedPyblishPluginMixin -): - """Collect all frames needed to publish expected frames""" - - order = pyblish.api.CollectorOrder + 0.5 - label = "Collect Expected Frames" - hosts = ["fusion"] - families = ["render"] - - def process(self, instance): - context = instance.context - - frame_start = context.data["frameStartHandle"] - frame_end = context.data["frameEndHandle"] - path = instance.data["path"] - output_dir = instance.data["outputDir"] - - basename = os.path.basename(path) - head, ext = os.path.splitext(basename) - files = [ - f"{head}{str(frame).zfill(4)}{ext}" - for frame in range(frame_start, frame_end + 1) - ] - repre = { - "name": ext[1:], - "ext": ext[1:], - "frameStart": f"%0{len(str(frame_end))}d" % frame_start, - "files": files, - "stagingDir": output_dir, - } - - self.set_representation_colorspace( - representation=repre, - context=context, - ) - - # review representation - if instance.data.get("review", False): - repre["tags"] = ["review"] - - # add the repre to the instance - if "representations" not in instance.data: - instance.data["representations"] = [] - instance.data["representations"].append(repre) diff --git a/openpype/hosts/fusion/plugins/publish/collect_fusion_version.py b/openpype/hosts/fusion/plugins/publish/collect_fusion_version.py deleted file mode 100644 index 65d8386f33..0000000000 --- a/openpype/hosts/fusion/plugins/publish/collect_fusion_version.py +++ /dev/null @@ -1,22 +0,0 @@ -import pyblish.api - - -class CollectFusionVersion(pyblish.api.ContextPlugin): - """Collect current comp""" - - order = pyblish.api.CollectorOrder - label = "Collect Fusion Version" - hosts = ["fusion"] - - def process(self, context): - """Collect all image sequence tools""" - - comp = context.data.get("currentComp") - if not comp: - raise RuntimeError("No comp previously collected, unable to " - "retrieve Fusion version.") - - version = comp.GetApp().Version - context.data["fusionVersion"] = version - - self.log.info("Fusion version: %s" % version) diff --git a/openpype/hosts/fusion/plugins/publish/collect_instances.py b/openpype/hosts/fusion/plugins/publish/collect_instances.py index af227f03db..4608f79420 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_instances.py +++ b/openpype/hosts/fusion/plugins/publish/collect_instances.py @@ -49,31 +49,3 @@ class CollectInstanceData(pyblish.api.InstancePlugin): if instance.data.get("review", False): self.log.info("Adding review family..") instance.data["families"].append("review") - - if instance.data["family"] == "render": - # TODO: This should probably move into a collector of - # its own for the "render" family - from openpype.hosts.fusion.api.lib import get_frame_path - comp = context.data["currentComp"] - - # This is only the case for savers currently but not - # for workfile instances. So we assume saver here. - tool = instance.data["transientData"]["tool"] - path = tool["Clip"][comp.TIME_UNDEFINED] - - filename = os.path.basename(path) - head, padding, tail = get_frame_path(filename) - ext = os.path.splitext(path)[1] - assert tail == ext, ("Tail does not match %s" % ext) - - instance.data.update({ - "path": path, - "outputDir": os.path.dirname(path), - "ext": ext, # todo: should be redundant? - - # Backwards compatibility: embed tool in instance.data - "tool": tool - }) - - # Add tool itself as member - instance.append(tool) diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py new file mode 100644 index 0000000000..87c1d952e8 --- /dev/null +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -0,0 +1,206 @@ +import os +from pprint import pformat +import attr +import pyblish.api + +from openpype.pipeline import publish +from openpype.pipeline.publish import RenderInstance +from openpype.hosts.fusion.api.lib import get_frame_path + + +@attr.s +class FusionRenderInstance(RenderInstance): + # extend generic, composition name is needed + fps = attr.ib(default=None) + projectEntity = attr.ib(default=None) + stagingDir = attr.ib(default=None) + app_version = attr.ib(default=None) + toolSaver = attr.ib(default=None) + workfileComp = attr.ib(default=None) + publish_attributes = attr.ib(default={}) + + +class CollectFusionRender( + publish.AbstractCollectRender, + publish.ColormanagedPyblishPluginMixin +): + + order = pyblish.api.CollectorOrder + 0.09 + label = "Collect Fusion Render" + hosts = ["fusion"] + + def get_instances(self, context): + + comp = context.data.get("currentComp") + comp_frame_format_prefs = comp.GetPrefs("Comp.FrameFormat") + aspect_x = comp_frame_format_prefs.get("AspectX") + aspect_y = comp_frame_format_prefs.get("AspectY") + + instances = [] + instances_to_remove = [] + + current_file = context.data["currentFile"] + version = context.data["version"] + + project_entity = context.data["projectEntity"] + + for inst in context: + if not inst.data.get("active", True): + continue + + family = inst.data["family"] + if family not in ["render"]: + continue + + task_name = inst.data.get("task") # legacy + tool = inst.data["transientData"]["tool"] + + instance_families = inst.data.get("families", []) + subset_name = inst.data["subset"] + instance = FusionRenderInstance( + family="render", + toolSaver=tool, + workfileComp=comp, + families=instance_families, + version=version, + time="", + source=current_file, + label="{} - {}".format(subset_name, family), + subset=subset_name, + asset=inst.data["asset"], + task=task_name, + attachTo=False, + setMembers='', + publish=True, + name=subset_name, + resolutionWidth=comp_frame_format_prefs.get("Width"), + resolutionHeight=comp_frame_format_prefs.get("Height"), + pixelAspect=aspect_x / aspect_y, + tileRendering=False, + tilesX=0, + tilesY=0, + review="review" in instance_families, + frameStart=context.data["frameStart"], + frameEnd=context.data["frameEnd"], + handleStart=context.data["handleStart"], + handleEnd=context.data["handleEnd"], + frameStep=1, + fps=comp_frame_format_prefs.get("Rate"), + app_version=comp.GetApp().Version, + publish_attributes=inst.data.get("publish_attributes", {}) + ) + + render_target = inst.data["creator_attributes"]["render_target"] + self.log.debug("render_target: '{}'".format(render_target)) + + if render_target == "local": + # for local renders + self._instance_data_local_update( + project_entity, instance, f"render.{render_target}") + + if render_target == "frames": + self._instance_data_local_update( + project_entity, instance, f"render.{render_target}") + + if render_target == "farm": + fam = "render.farm" + if fam not in instance.families: + instance.families.append(fam) + instance.toBeRenderedOn = "deadline" + instance.farm = True # to skip integrate + if "review" in instance.families: + # to skip ExtractReview locally + instance.families.remove("review") + + instances.append(instance) + instances_to_remove.append(inst) + + for instance in instances_to_remove: + context.remove(instance) + + return instances + + def post_collecting_action(self): + for instance in self._context: + if "render.frames" in instance.data.get("families", []): + self._update_for_frames(instance) + self.log.debug(pformat(instance.data)) + + def get_expected_files(self, render_instance): + """ + Returns list of rendered files that should be created by + Deadline. These are not published directly, they are source + for later 'submit_publish_job'. + + Args: + render_instance (RenderInstance): to pull anatomy and parts used + in url + + Returns: + (list) of absolute urls to rendered file + """ + start = render_instance.frameStart - render_instance.handleStart + end = render_instance.frameEnd + render_instance.handleEnd + + path = ( + render_instance.toolSaver["Clip"] + [render_instance.workfileComp.TIME_UNDEFINED] + ) + output_dir = os.path.dirname(path) + render_instance.outputDir = output_dir + + basename = os.path.basename(path) + + head, padding, ext = get_frame_path(basename) + + expected_files = [] + for frame in range(start, end + 1): + expected_files.append( + os.path.join( + output_dir, + f"{head}{str(frame).zfill(padding)}{ext}" + ) + ) + + return expected_files + + def _update_for_frames(self, instance): + """Update old saved instances to current publishing format""" + + expected_files = instance.data["expectedFiles"] + + start = instance.data["frameStart"] - instance.data["handleStart"] + + path = expected_files[0] + basename = os.path.basename(path) + staging_dir = os.path.dirname(path) + _, padding, ext = get_frame_path(basename) + + repre = { + "name": ext[1:], + "ext": ext[1:], + "frameStart": f"%0{padding}d" % start, + "files": [os.path.basename(f) for f in expected_files], + "stagingDir": staging_dir, + } + + self.set_representation_colorspace( + representation=repre, + context=instance.context, + ) + + # review representation + if instance.data.get("review", False): + repre["tags"] = ["review"] + + # add the repre to the instance + if "representations" not in instance.data: + instance.data["representations"] = [] + instance.data["representations"].append(repre) + + return instance + + def _instance_data_local_update(self, project_entity, instance, family): + instance.projectEntity = project_entity + if family not in instance.families: + instance.families.append(family) diff --git a/openpype/hosts/fusion/plugins/publish/collect_renders.py b/openpype/hosts/fusion/plugins/publish/collect_renders.py deleted file mode 100644 index b1c12c7393..0000000000 --- a/openpype/hosts/fusion/plugins/publish/collect_renders.py +++ /dev/null @@ -1,33 +0,0 @@ -import pyblish.api - - -class CollectFusionRenders(pyblish.api.InstancePlugin): - """Collect current saver node's render Mode - - Options: - local (Render locally) - frames (Use existing frames) - - """ - - order = pyblish.api.CollectorOrder + 0.4 - label = "Collect Renders" - hosts = ["fusion"] - families = ["render"] - - def process(self, instance): - render_target = instance.data["render_target"] - family = instance.data["family"] - - # add targeted family to families - instance.data["families"].append( - "{}.{}".format(family, render_target) - ) - if render_target == "farm": - if "review" in instance.data["families"]: - instance.data["families"].remove("review") - - # Farm rendering - instance.data["transfer"] = False - instance.data["farm"] = True - self.log.info("Farm rendering ON ...") From 7013be47de335bcb9d118d60e67d234360839d8b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 9 May 2023 17:21:16 +0200 Subject: [PATCH 025/189] Fusion: refactor validators to work with new collected data --- .../publish/validate_create_folder_checked.py | 2 +- .../validate_expected_frames_existence.py | 27 ++++++------------- .../validate_filename_has_extension.py | 4 +-- .../publish/validate_saver_has_input.py | 2 +- .../publish/validate_saver_passthrough.py | 2 +- .../publish/validate_unique_subsets.py | 2 +- 6 files changed, 14 insertions(+), 25 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py b/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py index 8a91f23578..82d34b0b5d 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py +++ b/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py @@ -21,7 +21,7 @@ class ValidateCreateFolderChecked(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance): - tool = instance[0] + tool = instance.data["toolSaver"] create_dir = tool.GetInput("CreateDir") if create_dir == 0.0: cls.log.error( diff --git a/openpype/hosts/fusion/plugins/publish/validate_expected_frames_existence.py b/openpype/hosts/fusion/plugins/publish/validate_expected_frames_existence.py index c208b8ef15..befaae13be 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_expected_frames_existence.py +++ b/openpype/hosts/fusion/plugins/publish/validate_expected_frames_existence.py @@ -14,7 +14,7 @@ class ValidateLocalFramesExistence(pyblish.api.InstancePlugin): order = pyblish.api.ValidatorOrder label = "Validate Expected Frames Exists" - families = ["render"] + families = ["render.frames"] hosts = ["fusion"] actions = [RepairAction, SelectInvalidAction] @@ -23,25 +23,15 @@ class ValidateLocalFramesExistence(pyblish.api.InstancePlugin): if non_existing_frames is None: non_existing_frames = [] - if instance.data.get("render_target") == "frames": - tool = instance[0] + if "render.frames" in instance.data.get("families", []): + tool = instance.data["toolSaver"] - frame_start = instance.data["frameStart"] - frame_end = instance.data["frameEnd"] - path = instance.data["path"] - output_dir = instance.data["outputDir"] + expected_files = instance.data["expectedFiles"] - basename = os.path.basename(path) - head, ext = os.path.splitext(basename) - files = [ - f"{head}{str(frame).zfill(4)}{ext}" - for frame in range(frame_start, frame_end + 1) - ] - - for file in files: - if not os.path.exists(os.path.join(output_dir, file)): + for file in expected_files: + if not os.path.exists(file): cls.log.error( - f"Missing file: {os.path.join(output_dir, file)}" + f"Missing file: {file}" ) non_existing_frames.append(file) @@ -67,8 +57,7 @@ class ValidateLocalFramesExistence(pyblish.api.InstancePlugin): def repair(cls, instance): invalid = cls.get_invalid(instance) if invalid: - tool = invalid[0] - + tool = instance.data["toolSaver"] # Change render target to local to render locally tool.SetData("openpype.creator_attributes.render_target", "local") diff --git a/openpype/hosts/fusion/plugins/publish/validate_filename_has_extension.py b/openpype/hosts/fusion/plugins/publish/validate_filename_has_extension.py index bbba2dde6e..1bf603e5b1 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_filename_has_extension.py +++ b/openpype/hosts/fusion/plugins/publish/validate_filename_has_extension.py @@ -30,11 +30,11 @@ class ValidateFilenameHasExtension(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance): - path = instance.data["path"] + path = instance.data["expectedFiles"][0] fname, ext = os.path.splitext(path) if not ext: - tool = instance[0] + tool = instance.data["toolSaver"] cls.log.error("%s has no extension specified" % tool.Name) return [tool] diff --git a/openpype/hosts/fusion/plugins/publish/validate_saver_has_input.py b/openpype/hosts/fusion/plugins/publish/validate_saver_has_input.py index e02125f531..b409608ec3 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_saver_has_input.py +++ b/openpype/hosts/fusion/plugins/publish/validate_saver_has_input.py @@ -20,7 +20,7 @@ class ValidateSaverHasInput(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance): - saver = instance[0] + saver = instance.data["toolSaver"] if not saver.Input.GetConnectedOutput(): return [saver] diff --git a/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py b/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py index 56f2e7e6b8..677861a654 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py +++ b/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py @@ -37,7 +37,7 @@ class ValidateSaverPassthrough(pyblish.api.ContextPlugin): def is_invalid(self, instance): - saver = instance[0] + saver = instance.data["toolSaver"] attr = saver.GetAttrs() active = not attr["TOOLB_PassThrough"] diff --git a/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py b/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py index 5b6ceb2fdb..6a65182fae 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py +++ b/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py @@ -43,7 +43,7 @@ class ValidateUniqueSubsets(pyblish.api.ContextPlugin): invalid.extend(instances) # Return tools for the invalid instances so they can be selected - invalid = [instance.data["tool"] for instance in invalid] + invalid = [instance.data["toolSaver"] for instance in invalid] return invalid From b8e8ce66606d643e82bccf3d8864063581754867 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 9 May 2023 17:22:01 +0200 Subject: [PATCH 026/189] fusion: rewriting render local to work with new instance data also adding colorspace data to representation --- .../plugins/publish/extract_render_local.py | 52 +++++++++++++++++-- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/extract_render_local.py b/openpype/hosts/fusion/plugins/publish/extract_render_local.py index 5a0140c525..c2e38884c7 100644 --- a/openpype/hosts/fusion/plugins/publish/extract_render_local.py +++ b/openpype/hosts/fusion/plugins/publish/extract_render_local.py @@ -1,8 +1,11 @@ +import os import logging import contextlib import pyblish.api -from openpype.hosts.fusion.api import comp_lock_and_undo_chunk +from openpype.pipeline import publish +from openpype.hosts.fusion.api import comp_lock_and_undo_chunk +from openpype.hosts.fusion.api.lib import get_frame_path log = logging.getLogger(__name__) @@ -38,7 +41,10 @@ def enabled_savers(comp, savers): saver.SetAttrs({"TOOLB_PassThrough": original_state}) -class FusionRenderLocal(pyblish.api.InstancePlugin): +class FusionRenderLocal( + pyblish.api.InstancePlugin, + publish.ColormanagedPyblishPluginMixin +): """Render the current Fusion composition locally.""" order = pyblish.api.ExtractorOrder - 0.2 @@ -52,6 +58,8 @@ class FusionRenderLocal(pyblish.api.InstancePlugin): # Start render self.render_once(context) + self._add_representation(instance) + # Log render status self.log.info( "Rendered '{nm}' for asset '{ast}' under the task '{tsk}'".format( @@ -71,11 +79,11 @@ class FusionRenderLocal(pyblish.api.InstancePlugin): savers_to_render = [ # Get the saver tool from the instance - instance[0] for instance in context if + instance.data["toolSaver"] for instance in context if # Only active instances instance.data.get("publish", True) and # Only render.local instances - "render.local" in instance.data["families"] + "render.local" in instance.data.get("families") ] if key not in context.data: @@ -107,3 +115,39 @@ class FusionRenderLocal(pyblish.api.InstancePlugin): if context.data[key] is False: raise RuntimeError("Comp render failed") + + def _add_representation(self, instance): + """Add representation to instance""" + + expected_files = instance.data["expectedFiles"] + + start = instance.data["frameStart"] - instance.data["handleStart"] + + path = expected_files[0] + _, padding, ext = get_frame_path(path) + + staging_dir = os.path.dirname(path) + + repre = { + "name": ext[1:], + "ext": ext[1:], + "frameStart": f"%0{padding}d" % start, + "files": [os.path.basename(f) for f in expected_files], + "stagingDir": staging_dir, + } + + self.set_representation_colorspace( + representation=repre, + context=instance.context, + ) + + # review representation + if instance.data.get("review", False): + repre["tags"] = ["review"] + + # add the repre to the instance + if "representations" not in instance.data: + instance.data["representations"] = [] + instance.data["representations"].append(repre) + + return instance From aace680fa19dee09dcb19d9692fd75ec4e6859ab Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 9 May 2023 17:22:31 +0200 Subject: [PATCH 027/189] fusion deadline, rewriting to new instance data --- .../plugins/publish/submit_fusion_deadline.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py index 2885d91d07..092c317ce3 100644 --- a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py @@ -96,8 +96,6 @@ class FusionSubmitDeadline( instance.data["suspend_publish"] = attribute_values[ "suspend_publish"] - instance.data["toBeRenderedOn"] = "deadline" - context = instance.context key = "__hasRun{}".format(self.__class__.__name__) @@ -132,8 +130,7 @@ class FusionSubmitDeadline( if not saver_instances: raise RuntimeError("No instances found for Deadline submittion") - fusion_version = int(context.data["fusionVersion"]) - comment = context.data.get("comment", "") + comment = instance.data.get("comment", "") deadline_user = context.data.get("deadlineUser", getpass.getuser()) script_path = context.data["currentFile"] @@ -201,7 +198,7 @@ class FusionSubmitDeadline( "FlowFile": script_path, # Mandatory for Deadline - "Version": str(fusion_version), + "Version": str(instance.data["app_version"]), # Render in high quality "HighQuality": True, @@ -225,7 +222,9 @@ class FusionSubmitDeadline( # Enable going to rendered frames from Deadline Monitor for index, instance in enumerate(saver_instances): - head, padding, tail = get_frame_path(instance.data["path"]) + head, padding, tail = get_frame_path( + instance.data["expectedFiles"][0] + ) path = "{}{}{}".format(head, "#" * padding, tail) folder, filename = os.path.split(path) payload["JobInfo"]["OutputDirectory%d" % index] = folder From b09efa96756ecb02cd0c1ae8b1183c692ff38147 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 10 May 2023 11:18:30 +0200 Subject: [PATCH 028/189] fusion: storing asset frame attribute at comp openpype_instance data --- openpype/hosts/fusion/api/lib.py | 7 ++++ .../publish/collect_comp_frame_range.py | 38 ++++++++++++++----- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/fusion/api/lib.py b/openpype/hosts/fusion/api/lib.py index 40cc4d2963..8f7b29f0c4 100644 --- a/openpype/hosts/fusion/api/lib.py +++ b/openpype/hosts/fusion/api/lib.py @@ -51,6 +51,12 @@ def update_frame_range(start, end, comp=None, set_render_range=True, "COMPN_GlobalStart": start - handle_start, "COMPN_GlobalEnd": end + handle_end } + frame_data = { + "frameStart": start, + "frameEnd": end, + "handleStart": handle_start, + "handleEnd": handle_end + } # set frame range if set_render_range: @@ -61,6 +67,7 @@ def update_frame_range(start, end, comp=None, set_render_range=True, with comp_lock_and_undo_chunk(comp): comp.SetAttrs(attrs) + comp.SetData("openpype_instance", frame_data) def set_asset_framerange(): diff --git a/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py b/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py index fbd7606cd7..2db0002ee6 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py +++ b/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py @@ -9,14 +9,20 @@ def get_comp_render_range(comp): global_start = comp_attrs["COMPN_GlobalStart"] global_end = comp_attrs["COMPN_GlobalEnd"] + frame_data = comp.GetData("openpype_instance") + handle_start = frame_data.get("handleStart", 0) + handle_end = frame_data.get("handleEnd", 0) + frame_start = frame_data.get("frameStart", 0) + frame_end = frame_data.get("frameEnd", 0) + # Whenever render ranges are undefined fall back # to the comp's global start and end if start == -1000000000: - start = global_start + start = frame_start if end == -1000000000: - end = global_end + end = frame_end - return start, end, global_start, global_end + return start, end, global_start, global_end, handle_start, handle_end class CollectFusionCompFrameRanges(pyblish.api.ContextPlugin): @@ -34,10 +40,22 @@ class CollectFusionCompFrameRanges(pyblish.api.ContextPlugin): comp = context.data["currentComp"] # Store comp render ranges - 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) - context.data["handleStart"] = int(start) - int(global_start) - context.data["handleEnd"] = int(global_end) - int(end) + ( + start, end, + global_start, + global_end, + handle_start, + handle_end + ) = get_comp_render_range(comp) + + data = {} + data["frameStart"] = int(start) + data["frameEnd"] = int(end) + data["frameStartHandle"] = int(global_start) + data["frameEndHandle"] = int(global_end) + data["handleStart"] = int(handle_start) + data["handleEnd"] = int(handle_end) + + self.log.debug("_ data: {}".format(data)) + + context.data.update(data) From 903d873dbe253e92b3b805f798b21c374af0a661 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 10 May 2023 11:19:08 +0200 Subject: [PATCH 029/189] fusion: frame ranges taken from instance --- .../hosts/fusion/plugins/publish/collect_render.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py index 87c1d952e8..5adb8a13f0 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_render.py +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -49,7 +49,7 @@ class CollectFusionRender( continue family = inst.data["family"] - if family not in ["render"]: + if family != "render": continue task_name = inst.data.get("task") # legacy @@ -80,10 +80,11 @@ class CollectFusionRender( tilesX=0, tilesY=0, review="review" in instance_families, - frameStart=context.data["frameStart"], - frameEnd=context.data["frameEnd"], - handleStart=context.data["handleStart"], - handleEnd=context.data["handleEnd"], + frameStart=inst.data["frameStart"], + frameEnd=inst.data["frameEnd"], + handleStart=inst.data["handleStart"], + handleEnd=inst.data["handleEnd"], + ignoreFrameHandleCheck=True, frameStep=1, fps=comp_frame_format_prefs.get("Rate"), app_version=comp.GetApp().Version, From 9cbbbe818ca7df153ea00f04a452705d9279efca Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 10 May 2023 11:19:40 +0200 Subject: [PATCH 030/189] deadline fusion: frame range taken from handles version --- .../deadline/plugins/publish/submit_fusion_deadline.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py index 092c317ce3..891bfde6c5 100644 --- a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py @@ -128,7 +128,7 @@ class FusionSubmitDeadline( saver_instances.append(instance) if not saver_instances: - raise RuntimeError("No instances found for Deadline submittion") + raise RuntimeError("No instances found for Deadline submission") comment = instance.data.get("comment", "") deadline_user = context.data.get("deadlineUser", getpass.getuser()) @@ -187,8 +187,8 @@ class FusionSubmitDeadline( "Plugin": "Fusion", "Frames": "{start}-{end}".format( - start=int(context.data["frameStart"]), - end=int(context.data["frameEnd"]) + start=int(instance.data["frameStartHandle"]), + end=int(instance.data["frameEndHandle"]) ), "Comment": comment, From 6ea1ab1c9f3da8d06bea10bb3b2b8a3305139120 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 10 May 2023 11:20:09 +0200 Subject: [PATCH 031/189] deadline submitter settings for aov filter --- openpype/settings/defaults/project_settings/deadline.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json index fdd70f1a44..3f114025f3 100644 --- a/openpype/settings/defaults/project_settings/deadline.json +++ b/openpype/settings/defaults/project_settings/deadline.json @@ -114,6 +114,9 @@ ], "max": [ ".*" + ], + "fusion": [ + ".*" ] } } From 9a6ae240e2fbafe693701cb791656e44e3440d74 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 11 May 2023 17:00:29 +0800 Subject: [PATCH 032/189] using currentfile for redshift renderer --- .../hosts/max/plugins/publish/collect_render.py | 6 ++++-- .../plugins/publish/submit_max_deadline.py | 16 +++++++++++----- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index b040467522..0d4dbc4521 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -5,7 +5,7 @@ import pyblish.api from pymxs import runtime as rt from openpype.pipeline import get_current_asset_name -from openpype.hosts.max.api.lib import get_max_version +from openpype.hosts.max.api.lib import get_max_version, get_current_renderer from openpype.hosts.max.api.lib_renderproducts import RenderProducts from openpype.client import get_last_version_by_subset_name @@ -38,7 +38,8 @@ class CollectRender(pyblish.api.InstancePlugin): version_doc = get_last_version_by_subset_name(project_name, instance.name, asset_id) - + renderer_class = get_current_renderer() + renderer = str(renderer_class).split(":")[0] self.log.debug("version_doc: {0}".format(version_doc)) version_int = 1 if version_doc: @@ -59,6 +60,7 @@ class CollectRender(pyblish.api.InstancePlugin): "source": filepath, "expectedFiles": render_layer_files, "plugin": "3dsmax", + "renderer": renderer, "frameStart": context.data['frameStart'], "frameEnd": context.data['frameEnd'], "version": version_int, diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index c728b6b9c7..0cf4990428 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -14,7 +14,6 @@ from openpype.pipeline import ( ) from openpype.settings import get_project_settings from openpype.hosts.max.api.lib import ( - get_current_renderer, get_multipass_setting ) from openpype.hosts.max.api.lib_rendersettings import RenderSettings @@ -157,6 +156,12 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, return plugin_payload + def from_published_scene(self, replace_in_path=True): + instance = self._instance + if instance.data["renderer"]== "Redshift_renderer": + file_path = self.scene_path + return file_path + def process_submission(self): instance = self._instance @@ -185,6 +190,8 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, instance = self._instance job_info = copy.deepcopy(self.job_info) plugin_info = copy.deepcopy(self.plugin_info) + if instance.data["renderer"] == "Redshift_Renderer": + self.log.debug("Using Redshift...published scene wont be used..") plugin_data = {} project_setting = get_project_settings( legacy_io.Session["AVALON_PROJECT"] @@ -202,7 +209,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, old_output_dir = os.path.dirname(expected_files[0]) output_beauty = RenderSettings().get_render_output(instance.name, old_output_dir) - filepath = self.from_published_scene() + filepath = self.scene_path def _clean_name(path): return os.path.splitext(os.path.basename(path))[0] @@ -214,9 +221,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, output_beauty = output_beauty.replace("\\", "/") plugin_data["RenderOutput"] = output_beauty - renderer_class = get_current_renderer() - renderer = str(renderer_class).split(":")[0] - if renderer in [ + if instance.data["renderer"] in [ "ART_Renderer", "Redshift_Renderer", "V_Ray_6_Hotfix_3", @@ -227,6 +232,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, render_elem_list = RenderSettings().get_render_element() for i, element in enumerate(render_elem_list): element = element.replace(orig_scene, new_scene) + element = element.replace("\\", "/") plugin_data["RenderElementOutputFilename%d" % i] = element # noqa self.log.debug("plugin data:{}".format(plugin_data)) From be386a36880a7868a4e46ba42fd49bfa08df6905 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 11 May 2023 17:05:56 +0800 Subject: [PATCH 033/189] hound fix --- .../modules/deadline/plugins/publish/submit_max_deadline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index 0cf4990428..a66d4d630a 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -158,7 +158,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, def from_published_scene(self, replace_in_path=True): instance = self._instance - if instance.data["renderer"]== "Redshift_renderer": + if instance.data["renderer"] == "Redshift_renderer": file_path = self.scene_path return file_path From fbee0a8b3c295dc55cd64a3037f517f88de150fa Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 12 May 2023 11:07:50 +0200 Subject: [PATCH 034/189] pr comments --- .../fusion/plugins/publish/collect_render.py | 3 --- .../validate_expected_frames_existence.py | 23 +++++++++---------- .../plugins/publish/submit_fusion_deadline.py | 3 --- 3 files changed, 11 insertions(+), 18 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py index 5adb8a13f0..4898226f03 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_render.py +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -1,5 +1,4 @@ import os -from pprint import pformat import attr import pyblish.api @@ -92,7 +91,6 @@ class CollectFusionRender( ) render_target = inst.data["creator_attributes"]["render_target"] - self.log.debug("render_target: '{}'".format(render_target)) if render_target == "local": # for local renders @@ -125,7 +123,6 @@ class CollectFusionRender( for instance in self._context: if "render.frames" in instance.data.get("families", []): self._update_for_frames(instance) - self.log.debug(pformat(instance.data)) def get_expected_files(self, render_instance): """ diff --git a/openpype/hosts/fusion/plugins/publish/validate_expected_frames_existence.py b/openpype/hosts/fusion/plugins/publish/validate_expected_frames_existence.py index befaae13be..aa89799867 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_expected_frames_existence.py +++ b/openpype/hosts/fusion/plugins/publish/validate_expected_frames_existence.py @@ -23,21 +23,20 @@ class ValidateLocalFramesExistence(pyblish.api.InstancePlugin): if non_existing_frames is None: non_existing_frames = [] - if "render.frames" in instance.data.get("families", []): - tool = instance.data["toolSaver"] + tool = instance.data["toolSaver"] - expected_files = instance.data["expectedFiles"] + expected_files = instance.data["expectedFiles"] - for file in expected_files: - if not os.path.exists(file): - cls.log.error( - f"Missing file: {file}" - ) - non_existing_frames.append(file) + for file in expected_files: + if not os.path.exists(file): + cls.log.error( + f"Missing file: {file}" + ) + non_existing_frames.append(file) - if len(non_existing_frames) > 0: - cls.log.error(f"Some of {tool.Name}'s files does not exist") - return [tool] + if len(non_existing_frames) > 0: + cls.log.error(f"Some of {tool.Name}'s files does not exist") + return [tool] def process(self, instance): non_existing_frames = [] diff --git a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py index 891bfde6c5..af4bd37302 100644 --- a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py @@ -1,7 +1,6 @@ import os import json import getpass -from pprint import pformat import requests @@ -90,8 +89,6 @@ class FusionSubmitDeadline( attribute_values = self.get_attr_values_from_data( instance.data) - self.log.debug(pformat(attribute_values)) - # add suspend_publish attributeValue to instance data instance.data["suspend_publish"] = attribute_values[ "suspend_publish"] From 801b904aea357ac0523fd4b75f0f2e7bd8ac5df9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 12 May 2023 11:29:29 +0200 Subject: [PATCH 035/189] rewriting logic for frame ranges --- openpype/hosts/fusion/api/lib.py | 7 ---- .../fusion/plugins/create/create_saver.py | 3 ++ .../publish/collect_comp_frame_range.py | 26 ++++----------- .../plugins/publish/collect_instances.py | 32 +++++++++++++++---- 4 files changed, 36 insertions(+), 32 deletions(-) diff --git a/openpype/hosts/fusion/api/lib.py b/openpype/hosts/fusion/api/lib.py index 8f7b29f0c4..40cc4d2963 100644 --- a/openpype/hosts/fusion/api/lib.py +++ b/openpype/hosts/fusion/api/lib.py @@ -51,12 +51,6 @@ def update_frame_range(start, end, comp=None, set_render_range=True, "COMPN_GlobalStart": start - handle_start, "COMPN_GlobalEnd": end + handle_end } - frame_data = { - "frameStart": start, - "frameEnd": end, - "handleStart": handle_start, - "handleEnd": handle_end - } # set frame range if set_render_range: @@ -67,7 +61,6 @@ def update_frame_range(start, end, comp=None, set_render_range=True, with comp_lock_and_undo_chunk(comp): comp.SetAttrs(attrs) - comp.SetData("openpype_instance", frame_data) def set_asset_framerange(): diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index ecdad30b4c..58ffbe928a 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -223,6 +223,9 @@ class CreateSaver(NewCreator): attr_defs = [ self._get_render_target_enum(), self._get_reviewable_bool(), + BoolDef( + "custom_range", label="Custom range", default=False, + ) ] return attr_defs diff --git a/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py b/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py index 2db0002ee6..38d6577667 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py +++ b/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py @@ -9,20 +9,14 @@ def get_comp_render_range(comp): global_start = comp_attrs["COMPN_GlobalStart"] global_end = comp_attrs["COMPN_GlobalEnd"] - frame_data = comp.GetData("openpype_instance") - handle_start = frame_data.get("handleStart", 0) - handle_end = frame_data.get("handleEnd", 0) - frame_start = frame_data.get("frameStart", 0) - frame_end = frame_data.get("frameEnd", 0) - # Whenever render ranges are undefined fall back # to the comp's global start and end if start == -1000000000: - start = frame_start + start = global_start if end == -1000000000: - end = frame_end + end = global_end - return start, end, global_start, global_end, handle_start, handle_end + return start, end, global_start, global_end class CollectFusionCompFrameRanges(pyblish.api.ContextPlugin): @@ -44,18 +38,12 @@ class CollectFusionCompFrameRanges(pyblish.api.ContextPlugin): start, end, global_start, global_end, - handle_start, - handle_end ) = get_comp_render_range(comp) data = {} - data["frameStart"] = int(start) - data["frameEnd"] = int(end) - data["frameStartHandle"] = int(global_start) - data["frameEndHandle"] = int(global_end) - data["handleStart"] = int(handle_start) - data["handleEnd"] = int(handle_end) - - self.log.debug("_ data: {}".format(data)) + data["compFrameStart"] = int(start) + data["compFrameEnd"] = int(end) + data["compFrameStartHandle"] = int(global_start) + data["compFrameEndHandle"] = int(global_end) context.data.update(data) diff --git a/openpype/hosts/fusion/plugins/publish/collect_instances.py b/openpype/hosts/fusion/plugins/publish/collect_instances.py index 4608f79420..9c27e3f027 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_instances.py +++ b/openpype/hosts/fusion/plugins/publish/collect_instances.py @@ -1,4 +1,6 @@ +from math import e import os +from turtle import st import pyblish.api @@ -24,6 +26,23 @@ class CollectInstanceData(pyblish.api.InstancePlugin): creator_attributes = instance.data["creator_attributes"] instance.data.update(creator_attributes) + # get asset frame ranges + start = context.data["frameStart"] + end = context.data["frameEnd"] + handle_start = context.data["handleStart"] + handle_end = context.data["handleEnd"] + start_handle = start - handle_start + end_handle = end + handle_end + + if creator_attributes["custom_range"]: + # get comp frame ranges + start = context.data["compFrameStart"] + end = context.data["compFrameEnd"] + handle_start = 0 + handle_end = 0 + start_handle = context.data["compFrameStartHandle"] + end_handle = context.data["compFrameEndHandle"] + # Include start and end render frame in label subset = instance.data["subset"] start = context.data["frameStart"] @@ -31,16 +50,17 @@ class CollectInstanceData(pyblish.api.InstancePlugin): label = "{subset} ({start}-{end})".format(subset=subset, start=int(start), end=int(end)) + instance.data.update({ "label": label, # todo: Allow custom frame range per instance - "frameStart": context.data["frameStart"], - "frameEnd": context.data["frameEnd"], - "frameStartHandle": context.data["frameStartHandle"], - "frameEndHandle": context.data["frameStartHandle"], - "handleStart": context.data["handleStart"], - "handleEnd": context.data["handleEnd"], + "frameStart": start, + "frameEnd": end, + "frameStartHandle": start_handle, + "frameEndHandle": end_handle, + "handleStart": handle_start, + "handleEnd": handle_end, "fps": context.data["fps"], }) From c80842f5a95aa88a6eaec0dd19b8326c89db760a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 12 May 2023 13:19:33 +0200 Subject: [PATCH 036/189] fusion: improving custom frame range --- .../fusion/plugins/create/create_saver.py | 21 +++++++++++++++++++ .../plugins/publish/collect_instances.py | 8 +++---- .../fusion/plugins/publish/collect_render.py | 2 +- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 58ffbe928a..860a873442 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -40,6 +40,11 @@ class CreateSaver(NewCreator): "{workdir}/renders/fusion/{subset}/{subset}.{frame}.{ext}") def create(self, subset_name, instance_data, pre_create_data): + self.pass_pre_attributes_to_instance( + instance_data, + pre_create_data + ) + instance_data.update({ "id": "pyblish.avalon.instance", "subset": subset_name @@ -215,6 +220,9 @@ class CreateSaver(NewCreator): attr_defs = [ self._get_render_target_enum(), self._get_reviewable_bool(), + BoolDef( + "custom_range", label="Custom range", default=False, + ) ] return attr_defs @@ -229,6 +237,19 @@ class CreateSaver(NewCreator): ] return attr_defs + def pass_pre_attributes_to_instance( + self, + instance_data, + pre_create_data, + keys=None + ): + if not keys: + keys = pre_create_data.keys() + + creator_attrs = instance_data["creator_attributes"] = {} + for pass_key in keys: + creator_attrs[pass_key] = pre_create_data[pass_key] + # These functions below should be moved to another file # so it can be used by other plugins. plugin.py ? diff --git a/openpype/hosts/fusion/plugins/publish/collect_instances.py b/openpype/hosts/fusion/plugins/publish/collect_instances.py index 9c27e3f027..997bd66e4a 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_instances.py +++ b/openpype/hosts/fusion/plugins/publish/collect_instances.py @@ -34,19 +34,17 @@ class CollectInstanceData(pyblish.api.InstancePlugin): start_handle = start - handle_start end_handle = end + handle_end - if creator_attributes["custom_range"]: + if creator_attributes.get("custom_range"): # get comp frame ranges start = context.data["compFrameStart"] end = context.data["compFrameEnd"] handle_start = 0 handle_end = 0 - start_handle = context.data["compFrameStartHandle"] - end_handle = context.data["compFrameEndHandle"] + start_handle = start + end_handle = end # Include start and end render frame in label subset = instance.data["subset"] - start = context.data["frameStart"] - end = context.data["frameEnd"] label = "{subset} ({start}-{end})".format(subset=subset, start=int(start), end=int(end)) diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py index 4898226f03..26355b24d3 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_render.py +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -83,7 +83,7 @@ class CollectFusionRender( frameEnd=inst.data["frameEnd"], handleStart=inst.data["handleStart"], handleEnd=inst.data["handleEnd"], - ignoreFrameHandleCheck=True, + ignoreFrameHandleCheck=(not inst.data.get("custom_range")), frameStep=1, fps=comp_frame_format_prefs.get("Rate"), app_version=comp.GetApp().Version, From 78b0e3daa1922402b4e8d2068208959cf22fa793 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 12 May 2023 13:35:15 +0200 Subject: [PATCH 037/189] removing unusable attribute GPU --- .../plugins/publish/submit_fusion_deadline.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py index af4bd37302..d51299506c 100644 --- a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py @@ -40,7 +40,6 @@ class FusionSubmitDeadline( group = "" department = "" limit_groups = {} - use_gpu = False env_allowed_keys = [] env_search_replace_values = {} @@ -69,11 +68,6 @@ class FusionSubmitDeadline( minimum=1, maximum=10 ), - BoolDef( - "use_gpu", - default=cls.use_gpu, - label="Use GPU" - ), BoolDef( "suspend_publish", default=False, @@ -206,11 +200,7 @@ class FusionSubmitDeadline( # Proxy: higher numbers smaller images for faster test renders # 1 = no proxy quality - "Proxy": 1, - - # using GPU by default - "UseGpu": attribute_values.get( - "use_gpu", self.use_gpu) + "Proxy": 1 }, # Mandatory for Deadline, may be empty From 5896c080132f4d62b3bc337ee586b40d7b4d991d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 12 May 2023 13:38:23 +0200 Subject: [PATCH 038/189] hound --- openpype/hosts/fusion/plugins/publish/collect_instances.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_instances.py b/openpype/hosts/fusion/plugins/publish/collect_instances.py index 997bd66e4a..5a6a918730 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_instances.py +++ b/openpype/hosts/fusion/plugins/publish/collect_instances.py @@ -1,7 +1,3 @@ -from math import e -import os -from turtle import st - import pyblish.api From 233c7b34545f9c6cfec2e87eb41983099bc96849 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Fri, 12 May 2023 14:12:40 +0200 Subject: [PATCH 039/189] Update openpype/hosts/fusion/plugins/create/create_saver.py Co-authored-by: Roy Nieterau --- openpype/hosts/fusion/plugins/create/create_saver.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 860a873442..4993c882de 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -174,7 +174,6 @@ class CreateSaver(NewCreator): tool.SetAttrs({"TOOLS_Name": subset}) def _collect_saver(self, tool): - self.log.info("Collecting saver..") attrs = tool.GetAttrs() keys = ["id", "asset", "subset", "task", "variant"] From 02279a51c8f97d0abf8e2d53d4d89acf1b13f1ce Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 12 May 2023 14:19:13 +0200 Subject: [PATCH 040/189] pr comments https://github.com/ynput/OpenPype/pull/4955#discussion_r1192264433 https://github.com/ynput/OpenPype/pull/4955#discussion_r1192267231 --- .../hosts/fusion/plugins/create/create_saver.py | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 4993c882de..caffb8e4a1 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -227,26 +227,15 @@ class CreateSaver(NewCreator): def get_instance_attr_defs(self): """Settings for publish page""" - attr_defs = [ - self._get_render_target_enum(), - self._get_reviewable_bool(), - BoolDef( - "custom_range", label="Custom range", default=False, - ) - ] - return attr_defs + return self.get_pre_create_attr_defs() def pass_pre_attributes_to_instance( self, instance_data, - pre_create_data, - keys=None + pre_create_data ): - if not keys: - keys = pre_create_data.keys() - creator_attrs = instance_data["creator_attributes"] = {} - for pass_key in keys: + for pass_key in pre_create_data.keys(): creator_attrs[pass_key] = pre_create_data[pass_key] # These functions below should be moved to another file From 425ddc7b2bb9399dd2d187761a4cc8755b000b79 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 12 May 2023 14:26:15 +0200 Subject: [PATCH 041/189] fusion: reversing toolSaver back to tool --- .../hosts/fusion/plugins/publish/collect_render.py | 14 ++++++++++---- .../fusion/plugins/publish/extract_render_local.py | 2 +- .../publish/validate_create_folder_checked.py | 2 +- .../publish/validate_expected_frames_existence.py | 4 ++-- .../publish/validate_filename_has_extension.py | 2 +- .../plugins/publish/validate_saver_has_input.py | 2 +- .../plugins/publish/validate_saver_passthrough.py | 2 +- .../plugins/publish/validate_unique_subsets.py | 2 +- 8 files changed, 18 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py index 26355b24d3..64d9aedc3b 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_render.py +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -14,7 +14,7 @@ class FusionRenderInstance(RenderInstance): projectEntity = attr.ib(default=None) stagingDir = attr.ib(default=None) app_version = attr.ib(default=None) - toolSaver = attr.ib(default=None) + tool = attr.ib(default=None) workfileComp = attr.ib(default=None) publish_attributes = attr.ib(default={}) @@ -58,7 +58,7 @@ class CollectFusionRender( subset_name = inst.data["subset"] instance = FusionRenderInstance( family="render", - toolSaver=tool, + tool=tool, workfileComp=comp, families=instance_families, version=version, @@ -111,6 +111,8 @@ class CollectFusionRender( # to skip ExtractReview locally instance.families.remove("review") + # add new instance to the list and remove the original + # instance since it is not needed anymore instances.append(instance) instances_to_remove.append(inst) @@ -141,7 +143,7 @@ class CollectFusionRender( end = render_instance.frameEnd + render_instance.handleEnd path = ( - render_instance.toolSaver["Clip"] + render_instance.tool["Clip"] [render_instance.workfileComp.TIME_UNDEFINED] ) output_dir = os.path.dirname(path) @@ -163,7 +165,11 @@ class CollectFusionRender( return expected_files def _update_for_frames(self, instance): - """Update old saved instances to current publishing format""" + """Updating instance for render.frames family + + Adding representation data to the instance. Also setting + colorspaceData to the representation based on file rules. + """ expected_files = instance.data["expectedFiles"] diff --git a/openpype/hosts/fusion/plugins/publish/extract_render_local.py b/openpype/hosts/fusion/plugins/publish/extract_render_local.py index c2e38884c7..f093f7793f 100644 --- a/openpype/hosts/fusion/plugins/publish/extract_render_local.py +++ b/openpype/hosts/fusion/plugins/publish/extract_render_local.py @@ -79,7 +79,7 @@ class FusionRenderLocal( savers_to_render = [ # Get the saver tool from the instance - instance.data["toolSaver"] for instance in context if + instance.data["tool"] for instance in context if # Only active instances instance.data.get("publish", True) and # Only render.local instances diff --git a/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py b/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py index 82d34b0b5d..35c92163eb 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py +++ b/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py @@ -21,7 +21,7 @@ class ValidateCreateFolderChecked(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance): - tool = instance.data["toolSaver"] + tool = instance.data["tool"] create_dir = tool.GetInput("CreateDir") if create_dir == 0.0: cls.log.error( diff --git a/openpype/hosts/fusion/plugins/publish/validate_expected_frames_existence.py b/openpype/hosts/fusion/plugins/publish/validate_expected_frames_existence.py index aa89799867..3f84f59678 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_expected_frames_existence.py +++ b/openpype/hosts/fusion/plugins/publish/validate_expected_frames_existence.py @@ -23,7 +23,7 @@ class ValidateLocalFramesExistence(pyblish.api.InstancePlugin): if non_existing_frames is None: non_existing_frames = [] - tool = instance.data["toolSaver"] + tool = instance.data["tool"] expected_files = instance.data["expectedFiles"] @@ -56,7 +56,7 @@ class ValidateLocalFramesExistence(pyblish.api.InstancePlugin): def repair(cls, instance): invalid = cls.get_invalid(instance) if invalid: - tool = instance.data["toolSaver"] + tool = instance.data["tool"] # Change render target to local to render locally tool.SetData("openpype.creator_attributes.render_target", "local") diff --git a/openpype/hosts/fusion/plugins/publish/validate_filename_has_extension.py b/openpype/hosts/fusion/plugins/publish/validate_filename_has_extension.py index 1bf603e5b1..537e43c875 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_filename_has_extension.py +++ b/openpype/hosts/fusion/plugins/publish/validate_filename_has_extension.py @@ -34,7 +34,7 @@ class ValidateFilenameHasExtension(pyblish.api.InstancePlugin): fname, ext = os.path.splitext(path) if not ext: - tool = instance.data["toolSaver"] + tool = instance.data["tool"] cls.log.error("%s has no extension specified" % tool.Name) return [tool] diff --git a/openpype/hosts/fusion/plugins/publish/validate_saver_has_input.py b/openpype/hosts/fusion/plugins/publish/validate_saver_has_input.py index b409608ec3..faf2102a8b 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_saver_has_input.py +++ b/openpype/hosts/fusion/plugins/publish/validate_saver_has_input.py @@ -20,7 +20,7 @@ class ValidateSaverHasInput(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance): - saver = instance.data["toolSaver"] + saver = instance.data["tool"] if not saver.Input.GetConnectedOutput(): return [saver] diff --git a/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py b/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py index 677861a654..9004976dc5 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py +++ b/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py @@ -37,7 +37,7 @@ class ValidateSaverPassthrough(pyblish.api.ContextPlugin): def is_invalid(self, instance): - saver = instance.data["toolSaver"] + saver = instance.data["tool"] attr = saver.GetAttrs() active = not attr["TOOLB_PassThrough"] diff --git a/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py b/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py index 6a65182fae..5b6ceb2fdb 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py +++ b/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py @@ -43,7 +43,7 @@ class ValidateUniqueSubsets(pyblish.api.ContextPlugin): invalid.extend(instances) # Return tools for the invalid instances so they can be selected - invalid = [instance.data["toolSaver"] for instance in invalid] + invalid = [instance.data["tool"] for instance in invalid] return invalid From 9d6cd8d2c83fe3d98aca92fea206c34b208f49a0 Mon Sep 17 00:00:00 2001 From: Seyedmohammadreza Hashemizadeh Date: Fri, 12 May 2023 16:21:58 +0200 Subject: [PATCH 042/189] add options --- openpype/hosts/maya/api/lib.py | 4 ++-- openpype/hosts/maya/plugins/load/load_reference.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index f814187cc1..d44f1754f9 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -3937,7 +3937,7 @@ def get_capture_preset(task_name, task_type, subset, project_settings, log): return capture_preset or {} -def create_rig_animation_instance(nodes, context, namespace, log=None): +def create_rig_animation_instance(nodes, context, namespace, options, log=None): """Create an animation publish instance for loaded rigs. See the RecreateRigAnimationInstance inventory action on how to use this @@ -3982,6 +3982,6 @@ def create_rig_animation_instance(nodes, context, namespace, log=None): creator_plugin, name=namespace, asset=asset, - options={"useSelection": True}, + options=options.update({"useSelection": True}), data={"dependencies": dependency} ) diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index 7d717dcd44..31b6e9d624 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -223,7 +223,7 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): def _post_process_rig(self, name, namespace, context, options): nodes = self[:] create_rig_animation_instance( - nodes, context, namespace, log=self.log + nodes, context, namespace, options, log=self.log ) def _lock_camera_transforms(self, nodes): From 454abe459ed834e6d10f014708c83a7142d1840d Mon Sep 17 00:00:00 2001 From: Seyedmohammadreza Hashemizadeh Date: Fri, 12 May 2023 17:53:05 +0200 Subject: [PATCH 043/189] add custom animation subset --- openpype/hosts/maya/api/lib.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index d44f1754f9..124e0e5b8a 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -3937,7 +3937,9 @@ def get_capture_preset(task_name, task_type, subset, project_settings, log): return capture_preset or {} -def create_rig_animation_instance(nodes, context, namespace, options, log=None): +def create_rig_animation_instance( + nodes, context, namespace, options, log=None +): """Create an animation publish instance for loaded rigs. See the RecreateRigAnimationInstance inventory action on how to use this @@ -3971,6 +3973,12 @@ def create_rig_animation_instance(nodes, context, namespace, options, log=None): asset = legacy_io.Session["AVALON_ASSET"] dependency = str(context["representation"]["_id"]) + custom_subset = options.get("animationSubsetName") + + if custom_subset: + rig_subset = context['subset']['name'] + namespace = namespace.replace(rig_subset, custom_subset) + if log: log.info("Creating subset: {}".format(namespace)) @@ -3982,6 +3990,6 @@ def create_rig_animation_instance(nodes, context, namespace, options, log=None): creator_plugin, name=namespace, asset=asset, - options=options.update({"useSelection": True}), + options={"useSelection": True}, data={"dependencies": dependency} ) From 2369d50224cd4768c182cee7004df4da7d381f27 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 15 May 2023 11:25:56 +0100 Subject: [PATCH 044/189] Skipping rendersetup for members. --- .../maya/plugins/publish/validate_instance_has_members.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/validate_instance_has_members.py b/openpype/hosts/maya/plugins/publish/validate_instance_has_members.py index 4870f27bff..fcafc2be79 100644 --- a/openpype/hosts/maya/plugins/publish/validate_instance_has_members.py +++ b/openpype/hosts/maya/plugins/publish/validate_instance_has_members.py @@ -14,6 +14,11 @@ class ValidateInstanceHasMembers(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance): + # Allow renderlayer and workfile to be empty + skip_families = ["workfile", "renderlayer", "rendersetup"] + if instance.data.get("family") in skip_families: + return + invalid = list() if not instance.data["setMembers"]: objectset_name = instance.data['name'] From b47d172944aa1e0039b3fb554093dbc7508b3033 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 15 May 2023 12:30:32 +0100 Subject: [PATCH 045/189] Move family check to process --- .../plugins/publish/validate_instance_has_members.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_instance_has_members.py b/openpype/hosts/maya/plugins/publish/validate_instance_has_members.py index fcafc2be79..63849cfd12 100644 --- a/openpype/hosts/maya/plugins/publish/validate_instance_has_members.py +++ b/openpype/hosts/maya/plugins/publish/validate_instance_has_members.py @@ -13,12 +13,6 @@ class ValidateInstanceHasMembers(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance): - - # Allow renderlayer and workfile to be empty - skip_families = ["workfile", "renderlayer", "rendersetup"] - if instance.data.get("family") in skip_families: - return - invalid = list() if not instance.data["setMembers"]: objectset_name = instance.data['name'] @@ -27,6 +21,10 @@ class ValidateInstanceHasMembers(pyblish.api.InstancePlugin): return invalid def process(self, instance): + # Allow renderlayer and workfile to be empty + skip_families = ["workfile", "renderlayer", "rendersetup"] + if instance.data.get("family") in skip_families: + return invalid = self.get_invalid(instance) if invalid: From 251470997783c56c0bf075fd1e206e37e1ccb9d3 Mon Sep 17 00:00:00 2001 From: Seyedmohammadreza Hashemizadeh Date: Mon, 15 May 2023 15:46:20 +0200 Subject: [PATCH 046/189] use custom namespace for animation instance --- openpype/hosts/maya/api/lib.py | 55 +++++++++++++++++++++++++++++-- openpype/hosts/maya/api/plugin.py | 38 --------------------- 2 files changed, 52 insertions(+), 41 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 124e0e5b8a..b84056f5ce 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -190,6 +190,44 @@ def maintained_selection(): cmds.select(clear=True) +def get_custom_namespace(custom_namespace): + """Return unique namespace. + + The input namespace can contain a single group + of '#' number tokens to indicate where the namespace's + unique index should go. The amount of tokens defines + the zero padding of the number, e.g ### turns into 001. + + Warning: Note that a namespace will always be + prefixed with a _ if it starts with a digit + + Example: + >>> get_custom_namespace("myspace_##_") + # myspace_01_ + >>> get_custom_namespace("##_myspace") + # _01_myspace + >>> get_custom_namespace("myspace##") + # myspace01 + + """ + split = re.split("([#]+)", custom_namespace, 1) + + if len(split) == 3: + base, padding, suffix = split + padding = "%0{}d".format(len(padding)) + else: + base = split[0] + padding = "%02d" # default padding + suffix = "" + + return unique_namespace( + base, + format=padding, + prefix="_" if not base or base[0].isdigit() else "", + suffix=suffix + ) + + def unique_namespace(namespace, format="%02d", prefix="", suffix=""): """Return unique namespace @@ -3974,10 +4012,21 @@ def create_rig_animation_instance( dependency = str(context["representation"]["_id"]) custom_subset = options.get("animationSubsetName") - if custom_subset: - rig_subset = context['subset']['name'] - namespace = namespace.replace(rig_subset, custom_subset) + formatting_data = { + "asset_name": context['asset']['name'], + "asset_type": context['asset']['type'], + "subset": context['subset']['name'], + "family": ( + context['subset']['data'].get('family') or + context['subset']['data']['families'][0] + ) + } + namespace = get_custom_namespace( + custom_subset.format( + **formatting_data + ) + ) if log: log.info("Creating subset: {}".format(namespace)) diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index 714278ba6c..3fce92db28 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -84,44 +84,6 @@ def get_reference_node_parents(ref): return parents -def get_custom_namespace(custom_namespace): - """Return unique namespace. - - The input namespace can contain a single group - of '#' number tokens to indicate where the namespace's - unique index should go. The amount of tokens defines - the zero padding of the number, e.g ### turns into 001. - - Warning: Note that a namespace will always be - prefixed with a _ if it starts with a digit - - Example: - >>> get_custom_namespace("myspace_##_") - # myspace_01_ - >>> get_custom_namespace("##_myspace") - # _01_myspace - >>> get_custom_namespace("myspace##") - # myspace01 - - """ - split = re.split("([#]+)", custom_namespace, 1) - - if len(split) == 3: - base, padding, suffix = split - padding = "%0{}d".format(len(padding)) - else: - base = split[0] - padding = "%02d" # default padding - suffix = "" - - return lib.unique_namespace( - base, - format=padding, - prefix="_" if not base or base[0].isdigit() else "", - suffix=suffix - ) - - class Creator(LegacyCreator): defaults = ['Main'] From 8ffe7f55522b364c07e6fd65cc8ad9cf5a125ed9 Mon Sep 17 00:00:00 2001 From: Seyedmohammadreza Hashemizadeh Date: Mon, 15 May 2023 16:27:03 +0200 Subject: [PATCH 047/189] fix function call --- openpype/hosts/maya/api/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index 3fce92db28..604ff101db 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -178,7 +178,7 @@ class ReferenceLoader(Loader): count = options.get("count") or 1 for c in range(0, count): - namespace = get_custom_namespace(custom_namespace) + namespace = lib.get_custom_namespace(custom_namespace) group_name = "{}:{}".format( namespace, custom_group_name From b192f8395d7cdd9c417e96e06a112e5761983b9d Mon Sep 17 00:00:00 2001 From: Seyedmohammadreza Hashemizadeh Date: Mon, 15 May 2023 16:34:37 +0200 Subject: [PATCH 048/189] update docstring --- openpype/hosts/maya/api/lib.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index b84056f5ce..56ba59186d 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -3987,6 +3987,7 @@ def create_rig_animation_instance( nodes (list): Member nodes of the rig instance. context (dict): Representation context of the rig container namespace (str): Namespace of the rig container + options (dict): Additional loader data log (logging.Logger, optional): Logger to log to if provided Returns: From f8ad6966fb53248e4c33ea2d3166c076fb078a42 Mon Sep 17 00:00:00 2001 From: Seyedmohammadreza Hashemizadeh Date: Mon, 15 May 2023 17:12:17 +0200 Subject: [PATCH 049/189] make options optional --- openpype/hosts/maya/api/lib.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 56ba59186d..7f160afd3e 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -3976,7 +3976,7 @@ def get_capture_preset(task_name, task_type, subset, project_settings, log): def create_rig_animation_instance( - nodes, context, namespace, options, log=None + nodes, context, namespace, options=None, log=None ): """Create an animation publish instance for loaded rigs. @@ -3987,13 +3987,16 @@ def create_rig_animation_instance( nodes (list): Member nodes of the rig instance. context (dict): Representation context of the rig container namespace (str): Namespace of the rig container - options (dict): Additional loader data + options (dict, optional): Additional loader data log (logging.Logger, optional): Logger to log to if provided Returns: None """ + if options is None: + options = {} + output = next((node for node in nodes if node.endswith("out_SET")), None) controls = next((node for node in nodes if From a051bb281a880c155c59083c153d9ed4e44e615f Mon Sep 17 00:00:00 2001 From: Mreza Hashemizadeh <68907585+mre7a@users.noreply.github.com> Date: Mon, 15 May 2023 17:22:40 +0200 Subject: [PATCH 050/189] Update openpype/hosts/maya/plugins/load/load_reference.py Co-authored-by: Toke Jepsen --- openpype/hosts/maya/plugins/load/load_reference.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index 31b6e9d624..f4a4a44344 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -223,7 +223,7 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): def _post_process_rig(self, name, namespace, context, options): nodes = self[:] create_rig_animation_instance( - nodes, context, namespace, options, log=self.log + nodes, context, namespace, options=options, log=self.log ) def _lock_camera_transforms(self, nodes): From 3df689787699f6f51557e058357273689a64cbf0 Mon Sep 17 00:00:00 2001 From: Seyedmohammadreza Hashemizadeh Date: Mon, 15 May 2023 18:01:52 +0200 Subject: [PATCH 051/189] Add some documentations --- website/docs/admin_hosts_maya.md | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/website/docs/admin_hosts_maya.md b/website/docs/admin_hosts_maya.md index f0b8710246..700822843f 100644 --- a/website/docs/admin_hosts_maya.md +++ b/website/docs/admin_hosts_maya.md @@ -247,15 +247,24 @@ Fill in the necessary fields (the optional fields are regex filters) ![new place holder](assets/maya-placeholder_new.png) - - Builder type: Whether the the placeholder should load current asset representations or linked assets representations + - ***Builder type***: Whether the the placeholder should load current asset representations or linked assets representations - - Representation: Representation that will be loaded (ex: ma, abc, png, etc...) + - ***Representation***: Representation that will be loaded (ex: ma, abc, png, etc...) - - Family: Family of the representation to load (main, look, image, etc ...) + - ***Family***: Family of the representation to load (main, look, image, etc ...) - - Loader: Placeholder loader name that will be used to load corresponding representations + - ***Loader***: Placeholder loader name that will be used to load corresponding representations + + - ***Order***: Priority for current placeholder loader (priority is lowest first, highest last) + + - ***Loader arguments***: Loader arguments dictionary can be used to pass optional data to loaders. + One use case is to define a custom Subset name for the animation instances created while loading Rig references.This follows the custom namespace system used by loaders. + + **Example** + ``` + {"animationSubsetName": "{asset_name}_animation_{subset}_##_"} + ``` - - Order: Priority for current placeholder loader (priority is lowest first, highet last) - **Save your template** From f51af7de278e77724c058196eed7dd487da07dfe Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 15 May 2023 21:54:01 +0200 Subject: [PATCH 052/189] fusion: renaming comp frame range related attributes --- .../fusion/plugins/publish/collect_comp_frame_range.py | 8 ++++---- .../hosts/fusion/plugins/publish/collect_instances.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py b/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py index 38d6577667..08bdad3120 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py +++ b/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py @@ -41,9 +41,9 @@ class CollectFusionCompFrameRanges(pyblish.api.ContextPlugin): ) = get_comp_render_range(comp) data = {} - data["compFrameStart"] = int(start) - data["compFrameEnd"] = int(end) - data["compFrameStartHandle"] = int(global_start) - data["compFrameEndHandle"] = int(global_end) + data["renderFrameStart"] = int(start) + data["renderFrameEnd"] = int(end) + data["compFrameStart"] = int(global_start) + data["compFrameEnd"] = int(global_end) context.data.update(data) diff --git a/openpype/hosts/fusion/plugins/publish/collect_instances.py b/openpype/hosts/fusion/plugins/publish/collect_instances.py index 5a6a918730..c1c23ec570 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_instances.py +++ b/openpype/hosts/fusion/plugins/publish/collect_instances.py @@ -32,8 +32,8 @@ class CollectInstanceData(pyblish.api.InstancePlugin): if creator_attributes.get("custom_range"): # get comp frame ranges - start = context.data["compFrameStart"] - end = context.data["compFrameEnd"] + start = context.data["renderFrameStart"] + end = context.data["renderFrameEnd"] handle_start = 0 handle_end = 0 start_handle = start From 478c85d7cc96188db55af4cbc8c1a2479ff8717e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 15 May 2023 22:09:18 +0200 Subject: [PATCH 053/189] Fusion: renaming confusing attribute name and label --- openpype/hosts/fusion/plugins/create/create_saver.py | 4 +++- openpype/hosts/fusion/plugins/publish/collect_instances.py | 2 +- openpype/hosts/fusion/plugins/publish/collect_render.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 1a60526e42..67b1465ec7 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -200,7 +200,9 @@ class CreateSaver(NewCreator): self._get_render_target_enum(), self._get_reviewable_bool(), BoolDef( - "custom_range", label="Custom range", default=False, + "viewer_render_range", + label="Viewer render in/out", + default=False, ) ] return attr_defs diff --git a/openpype/hosts/fusion/plugins/publish/collect_instances.py b/openpype/hosts/fusion/plugins/publish/collect_instances.py index c1c23ec570..6887f4f4e9 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_instances.py +++ b/openpype/hosts/fusion/plugins/publish/collect_instances.py @@ -30,7 +30,7 @@ class CollectInstanceData(pyblish.api.InstancePlugin): start_handle = start - handle_start end_handle = end + handle_end - if creator_attributes.get("custom_range"): + if creator_attributes.get("viewer_render_range"): # get comp frame ranges start = context.data["renderFrameStart"] end = context.data["renderFrameEnd"] diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py index 64d9aedc3b..c3ae9f381d 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_render.py +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -83,7 +83,7 @@ class CollectFusionRender( frameEnd=inst.data["frameEnd"], handleStart=inst.data["handleStart"], handleEnd=inst.data["handleEnd"], - ignoreFrameHandleCheck=(not inst.data.get("custom_range")), + ignoreFrameHandleCheck=(not inst.data.get("viewer_render_range")), frameStep=1, fps=comp_frame_format_prefs.get("Rate"), app_version=comp.GetApp().Version, From e14e0f5a40c6a48ffc9302544c99e0c3f757b2b6 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 15 May 2023 22:11:35 +0200 Subject: [PATCH 054/189] hound --- openpype/hosts/fusion/plugins/publish/collect_render.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py index c3ae9f381d..6956b566ad 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_render.py +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -83,7 +83,8 @@ class CollectFusionRender( frameEnd=inst.data["frameEnd"], handleStart=inst.data["handleStart"], handleEnd=inst.data["handleEnd"], - ignoreFrameHandleCheck=(not inst.data.get("viewer_render_range")), + ignoreFrameHandleCheck=( + not inst.data.get("viewer_render_range")), frameStep=1, fps=comp_frame_format_prefs.get("Rate"), app_version=comp.GetApp().Version, From 11d47eb9407be381a6a10731b683886f9f26f0da Mon Sep 17 00:00:00 2001 From: Petr Dvorak Date: Tue, 16 May 2023 10:28:24 +0200 Subject: [PATCH 055/189] Company name and URL changed --- inno_setup.iss | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/inno_setup.iss b/inno_setup.iss index 3adde52a8b..418bedbd4d 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -14,10 +14,10 @@ AppId={{B9E9DF6A-5BDA-42DD-9F35-C09D564C4D93} AppName={#MyAppName} AppVersion={#AppVer} AppVerName={#MyAppName} version {#AppVer} -AppPublisher=Orbi Tools s.r.o -AppPublisherURL=http://pype.club -AppSupportURL=http://pype.club -AppUpdatesURL=http://pype.club +AppPublisher=Ynput s.r.o +AppPublisherURL=https://ynput.io +AppSupportURL=https://ynput.io +AppUpdatesURL=https://ynput.io DefaultDirName={autopf}\{#MyAppName}\{#AppVer} UsePreviousAppDir=no DisableProgramGroupPage=yes From 10b953f8bf0a16c2f49f5d89d3f0df6030ea85c9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 16 May 2023 11:34:54 +0200 Subject: [PATCH 056/189] fusion: converting frame range source to enum --- .../fusion/plugins/create/create_saver.py | 19 +++++++++++++------ .../plugins/publish/collect_instances.py | 5 +++-- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 67b1465ec7..80f60a0c6e 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -199,11 +199,7 @@ class CreateSaver(NewCreator): attr_defs = [ self._get_render_target_enum(), self._get_reviewable_bool(), - BoolDef( - "viewer_render_range", - label="Viewer render in/out", - default=False, - ) + self._get_frame_range_enum() ] return attr_defs @@ -222,7 +218,6 @@ class CreateSaver(NewCreator): # These functions below should be moved to another file # so it can be used by other plugins. plugin.py ? - def _get_render_target_enum(self): rendering_targets = { "local": "Local machine rendering", @@ -235,6 +230,18 @@ class CreateSaver(NewCreator): "render_target", items=rendering_targets, label="Render target" ) + def _get_frame_range_enum(self): + frame_range_options = { + "asset_db": "From asset database", + "viewer_render_range": "From viewer render in/out" + } + + return EnumDef( + "frame_range_source", + items=frame_range_options, + label="Frame range source" + ) + def _get_reviewable_bool(self): return BoolDef( "review", diff --git a/openpype/hosts/fusion/plugins/publish/collect_instances.py b/openpype/hosts/fusion/plugins/publish/collect_instances.py index 6887f4f4e9..61ce10d32f 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_instances.py +++ b/openpype/hosts/fusion/plugins/publish/collect_instances.py @@ -20,6 +20,7 @@ class CollectInstanceData(pyblish.api.InstancePlugin): # Include creator attributes directly as instance data creator_attributes = instance.data["creator_attributes"] + frame_range_source = creator_attributes.get("frame_range_source") instance.data.update(creator_attributes) # get asset frame ranges @@ -30,8 +31,8 @@ class CollectInstanceData(pyblish.api.InstancePlugin): start_handle = start - handle_start end_handle = end + handle_end - if creator_attributes.get("viewer_render_range"): - # get comp frame ranges + if frame_range_source == "viewer_render_range": + # set comp render frame ranges start = context.data["renderFrameStart"] end = context.data["renderFrameEnd"] handle_start = 0 From 2d6919297cdc370bc43a4cc76390021e2ed8564b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 16 May 2023 11:40:00 +0200 Subject: [PATCH 057/189] fusion: adding comp range option --- .../hosts/fusion/plugins/create/create_saver.py | 3 ++- .../fusion/plugins/publish/collect_instances.py | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 80f60a0c6e..d5e77730c8 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -233,7 +233,8 @@ class CreateSaver(NewCreator): def _get_frame_range_enum(self): frame_range_options = { "asset_db": "From asset database", - "viewer_render_range": "From viewer render in/out" + "viewer_render_range": "From viewer render in/out", + "comp_range": "From composition timeline" } return EnumDef( diff --git a/openpype/hosts/fusion/plugins/publish/collect_instances.py b/openpype/hosts/fusion/plugins/publish/collect_instances.py index 61ce10d32f..59ff52f5b2 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_instances.py +++ b/openpype/hosts/fusion/plugins/publish/collect_instances.py @@ -40,6 +40,19 @@ class CollectInstanceData(pyblish.api.InstancePlugin): start_handle = start end_handle = end + if frame_range_source == "comp_range": + comp_start = context.data["compFrameStart"] + comp_end = context.data["compFrameEnd"] + render_start = context.data["renderFrameStart"] + render_end = context.data["renderFrameEnd"] + # set comp frame ranges + start = render_start + end = render_end + handle_start = render_start - comp_start + handle_end = comp_end - render_end + start_handle = comp_start + end_handle = comp_end + # Include start and end render frame in label subset = instance.data["subset"] label = "{subset} ({start}-{end})".format(subset=subset, From 16f24c253803026ab0f768ef5c3dca1320561ca4 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 16 May 2023 22:48:02 +0800 Subject: [PATCH 058/189] fix the bug of fbx loaders --- openpype/hosts/max/plugins/load/load_camera_fbx.py | 10 +++++++--- openpype/hosts/max/plugins/load/load_model_fbx.py | 9 +++++++-- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_camera_fbx.py b/openpype/hosts/max/plugins/load/load_camera_fbx.py index 3a6947798e..ce4dec32a0 100644 --- a/openpype/hosts/max/plugins/load/load_camera_fbx.py +++ b/openpype/hosts/max/plugins/load/load_camera_fbx.py @@ -36,12 +36,16 @@ importFile @"{filepath}" #noPrompt using:FBXIMP self.log.debug(f"Executing command: {fbx_import_cmd}") rt.execute(fbx_import_cmd) - container_name = f"{name}_CON" + container = rt.getNodeByName(f"{name}") + if not container: + container = rt.container() + container.name = f"{name}" - asset = rt.getNodeByName(f"{name}") + for selection in rt.getCurrentSelection(): + selection.Parent = container return containerise( - name, [asset], context, loader=self.__class__.__name__) + name, [container], context, loader=self.__class__.__name__) def update(self, container, representation): from pymxs import runtime as rt diff --git a/openpype/hosts/max/plugins/load/load_model_fbx.py b/openpype/hosts/max/plugins/load/load_model_fbx.py index 88b8f1ed89..7532e3a8a0 100644 --- a/openpype/hosts/max/plugins/load/load_model_fbx.py +++ b/openpype/hosts/max/plugins/load/load_model_fbx.py @@ -36,11 +36,16 @@ importFile @"{filepath}" #noPrompt using:FBXIMP self.log.debug(f"Executing command: {fbx_import_cmd}") rt.execute(fbx_import_cmd) + container = rt.getNodeByName(f"{name}") + if not container: + container = rt.container() + container.name = f"{name}" - asset = rt.getNodeByName(f"{name}") + for selection in rt.getCurrentSelection(): + selection.Parent = container return containerise( - name, [asset], context, loader=self.__class__.__name__) + name, [container], context, loader=self.__class__.__name__) def update(self, container, representation): from pymxs import runtime as rt From c78a74248702d47cf33ff55e38bf8def1f86ec17 Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov Date: Wed, 17 May 2023 10:02:59 +0300 Subject: [PATCH 059/189] add logs, remove adding PYTHONHOME to PATH --- .../Support/logs/davinci_resolve.log | 443 ++++++++++++++++++ .../hosts/resolve/hooks/pre_resolve_setup.py | 14 +- 2 files changed, 449 insertions(+), 8 deletions(-) create mode 100644 igniter/#ProgramData#Blackmagic Design/DaVinci Resolve/Support/logs/davinci_resolve.log diff --git a/igniter/#ProgramData#Blackmagic Design/DaVinci Resolve/Support/logs/davinci_resolve.log b/igniter/#ProgramData#Blackmagic Design/DaVinci Resolve/Support/logs/davinci_resolve.log new file mode 100644 index 0000000000..8f1a3b712e --- /dev/null +++ b/igniter/#ProgramData#Blackmagic Design/DaVinci Resolve/Support/logs/davinci_resolve.log @@ -0,0 +1,443 @@ +[0x00004fe8] | Undefined | INFO | 2023-05-17 09:46:08,249 | -------------------------------------------------------------------------------- +[0x00004fe8] | Undefined | INFO | 2023-05-17 09:46:08,250 | Loaded log config from C:\Users\videopro\AppData\Roaming\Blackmagic Design\DaVinci Resolve\Preferences\log-conf.xml +[0x00004fe8] | Undefined | INFO | 2023-05-17 09:46:08,250 | -------------------------------------------------------------------------------- +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:08,251 | Running DaVinci Resolve Studio v18.1.2.0006 (Windows/MSVC x86_64) +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:08,251 | BMD_BUILD_UUID 3ff36663-26c9-45d9-8506-101676c881e0 +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:08,251 | BMD_GIT_COMMIT a3be29d0542aeafcb1b0933bb5ace426aa7d047d +[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,259 | Starting GPUDetect 1.2_3-a1 +[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | Done in 161 ms. +[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | Detected System: +[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | - OS: Windows 10 Pro (Build 19045) +[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | - Model: ASUSTeK ROG STRIX B550-F GAMING (WI-FI) +[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | - System ID: 838d5045-58b0-44ed-854c-19be5b814d6f +[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | - CPU: AMD Ryzen 7 3700X, 16 threads, x86-64 +[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | - RAM: 16.6 GiB used of 63.9 GiB +[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | - NVIDIA GPU Driver: 527.56, supports CUDA 12.0 +[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | Detected 1 GPUs: +[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,423 | - "NVIDIA GeForce RTX 2060 SUPER" (gpu:a24a3d50.83f1fdb7) <- Main Display GPU +[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,423 | Discrete, 2.0 GiB used of 7.6 GiB VRAM, PCI:8:0 (x16) +[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,423 | Matches: CUDA, DirectX, NVAPI, NVML, OpenCL, Win32 +[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,423 | Detected 1 monitors: +[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,423 | - "Generic PnP Monitor" <- Main Monitor +[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,423 | 3840x2160, connected to "NVIDIA GeForce RTX 2060 SUPER" (gpu:a24a3d50.83f1fdb7) +[0x00004fe8] | Main.GPUConfig | INFO | 2023-05-17 09:46:08,424 | Selected compute API: CUDA +[0x00004fe8] | Main.GPUConfig | INFO | 2023-05-17 09:46:08,424 | Automatic GPU Selection: +[0x00004fe8] | Main.GPUConfig | INFO | 2023-05-17 09:46:08,424 | - "NVIDIA GeForce RTX 2060 SUPER" (gpu:a24a3d50.83f1fdb7) +[0x00004fe8] | IO | INFO | 2023-05-17 09:46:08,438 | RED InitializeSdk with library path at C:\ProgramData\Blackmagic Design\DaVinci Resolve\Support\Libraries +[0x00004fe8] | IO | INFO | 2023-05-17 09:46:08,642 | R3DAPI 8.3.1-52407 (20220725 Wx64S) R3DSDK 8.3.1-52407 (20220725 Wx64D C3B3) RED CUDA 8.3.1-52408 (20220725) [C:\ProgramData\Blackmagic Design\DaVinci Resolve\Support\Libraries\] init is successful +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:08,642 | 0 RED rocket cards available +[0x00004fe8] | SyManager.DeckLink | ERROR | 2023-05-17 09:46:08,645 | Failed to create instance. +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:08,645 | Decklink model name: '', version: '' +[0x00004fe8] | DVIP | INFO | 2023-05-17 09:46:08,645 | DVIP release/18.1.2 build 2 (86acfdf407856b6cd8daf2517ab0b44f1efc332f). Release, version 18.1.2. +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:08,645 | Resolve Module Handle 00007FF6A4B50000 +[0x00001238] | IO | INFO | 2023-05-17 09:46:08,646 | Using DNxHR library v2.7.3.27r +[0x00002044] | Fusion | INFO | 2023-05-17 09:46:09,093 | Fusion Build: 008b13c7_0004 (Dec 7 2022 15:23:40) +[0x00005f70] | Fusion | INFO | 2023-05-17 09:46:09,114 | fusionsystem: = "C:\Program Files\Blackmagic Design\DaVinci Resolve\fusionsystem.dll" +[0x00001238] | IO | INFO | 2023-05-17 09:46:09,117 | NVDEC is using upto (1023) MB +[0x00001238] | IO | INFO | 2023-05-17 09:46:09,119 | NVDEC decodes H264, chroma 4:2:0, bitdepth 8, upto 4096 x 4096 +[0x00001238] | IO | INFO | 2023-05-17 09:46:09,121 | NVDEC decodes HEVC, chroma 4:2:0, bitdepth 8, upto 8192 x 8192 +[0x00001238] | IO | INFO | 2023-05-17 09:46:09,123 | NVDEC decodes HEVC, chroma 4:2:0, bitdepth 10, upto 8192 x 8192 +[0x00001238] | IO | INFO | 2023-05-17 09:46:09,127 | NVDEC decodes HEVC, chroma 4:2:0, bitdepth 12, upto 8192 x 8192 +[0x00001238] | IO | INFO | 2023-05-17 09:46:09,128 | NVDEC decodes HEVC, chroma 4:4:4, bitdepth 8, upto 8192 x 8192 +[0x00001238] | IO | INFO | 2023-05-17 09:46:09,130 | NVDEC decodes HEVC, chroma 4:4:4, bitdepth 10, upto 8192 x 8192 +[0x00001238] | IO | INFO | 2023-05-17 09:46:09,131 | NVDEC decodes HEVC, chroma 4:4:4, bitdepth 12, upto 8192 x 8192 +[0x00001238] | IO | INFO | 2023-05-17 09:46:09,133 | NVDEC decodes VP9, chroma 4:2:0, bitdepth 8, upto 8192 x 8192 +[0x00001238] | IO | INFO | 2023-05-17 09:46:09,134 | NVDEC decodes VP9, chroma 4:2:0, bitdepth 10, upto 8192 x 8192 +[0x00005f70] | Fusion | INFO | 2023-05-17 09:46:09,148 | FusionLibs: = "C:\Program Files\Blackmagic Design\DaVinci Resolve\" +[0x00005f70] | Fusion | INFO | 2023-05-17 09:46:09,148 | UserData: = "C:\Users\videopro\AppData\Roaming\Blackmagic Design\DaVinci Resolve\Support\Fusion" +[0x00005f70] | Fusion | INFO | 2023-05-17 09:46:09,148 | Profiles: = "UserData:Profiles\" +[0x00001238] | IO | INFO | 2023-05-17 09:46:09,148 | Nvidia GPU (0) is initialised as decoding and encoding device. +[0x00001238] | IO | INFO | 2023-05-17 09:46:09,182 | IO codec library load completed in 536 ms. +[0x00005cbc] | SyManager | ERROR | 2023-05-17 09:46:09,202 | BlackmagicIDHelper::GetProjectLibraries() - Access token is empty +[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:09,268 | Loading dblist file: C:\Users\videopro\AppData\Roaming\Blackmagic Design\DaVinci Resolve\Preferences\dblist.conf +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:09,314 | Finished loading Application style sheet +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:09,396 | Show splash screen +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:09,396 | Show splash screen message: Starting Up +[0x00004fe8] | Fusion | INFO | 2023-05-17 09:46:09,398 | Module Handle 0000023A5FC80000 fusionsystem +[0x0000204c] | Fusion | INFO | 2023-05-17 09:46:09,410 | Module Handle 0000023A6C160000 C:\Program Files\Blackmagic Design\DaVinci Resolve\fusiongraphics.dll +[0x00004f28] | Fusion | INFO | 2023-05-17 09:46:09,437 | Module Handle 0000023A6FB90000 fusionoperators.dll +[0x00005f70] | Fusion | INFO | 2023-05-17 09:46:09,462 | Module Handle 0000023A70490000 fusioncontrols.dll +[0x00007b34] | Fusion | INFO | 2023-05-17 09:46:09,488 | Module Handle 0000023A70870000 3d.plugin +[0x0000772c] | Fusion | INFO | 2023-05-17 09:46:09,510 | Module Handle 0000023A70B50000 dimension.plugin +[0x00003934] | Fusion | INFO | 2023-05-17 09:46:09,528 | Module Handle 0000023A70F70000 alembic.plugin +[0x0000204c] | Fusion | INFO | 2023-05-17 09:46:09,551 | Module Handle 0000023A71320000 fbx.plugin +[0x000078cc] | Fusion | INFO | 2023-05-17 09:46:09,566 | Module Handle 0000023A464C0000 fuses.plugin +[0x00004f28] | Fusion | INFO | 2023-05-17 09:46:09,582 | Module Handle 0000023A71A70000 opencolorio.plugin +[0x00005f70] | Fusion | INFO | 2023-05-17 09:46:09,599 | Module Handle 0000023A6ADF0000 openfx.plugin +[0x00007b34] | Fusion | INFO | 2023-05-17 09:46:09,615 | Module Handle 0000023A68DA0000 openvr.plugin +[0x0000772c] | Fusion | INFO | 2023-05-17 09:46:09,632 | Module Handle 0000023A6AE60000 paint.plugin +[0x00003934] | Fusion | INFO | 2023-05-17 09:46:09,653 | Module Handle 0000023A6AED0000 particles.plugin +[0x0000204c] | Fusion | INFO | 2023-05-17 09:46:09,673 | Module Handle 0000023A720D0000 text.plugin +[0x000078cc] | Fusion | INFO | 2023-05-17 09:46:09,690 | Module Handle 0000023A6CB80000 utilities.plugin +[0x00004f28] | Fusion | INFO | 2023-05-17 09:46:09,715 | Module Handle 00007FF89F590000 KrokodoveFu16.plugin +[0x00005f70] | Fusion | INFO | 2023-05-17 09:46:09,717 | Module Handle 00007FF89F330000 KrokodoveFu17.plugin +[0x00004f28] | OpenFX | INFO | 2023-05-17 09:46:09,817 | No context is available for com.absoft.NeatVideo5 +[0x00007fc8] | Main | INFO | 2023-05-17 09:46:09,828 | Started listener socket at port 15000 +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:10,308 | Show splash screen message: Checking Licenses +[0x00004fe8] | BtCommon | INFO | 2023-05-17 09:46:10,532 | Memory config: reserved=12270M pinned=8000M log=0 +[0x00004fe8] | BtCommon | INFO | 2023-05-17 09:46:10,532 | Using default pooled memory manager +[0x00003798] | LeManager | INFO | 2023-05-17 09:46:10,533 | 521, 29 +[0x00008180] | BtCommon | INFO | 2023-05-17 09:46:10,533 | BtResourceManager Process Thread Started +[0x00004fe8] | IO | INFO | 2023-05-17 09:46:10,609 | WMF encoder cnt for SW (hvc1) is (0) +[0x00004fe8] | IO | INFO | 2023-05-17 09:46:10,624 | WMF encoder cnt for HW (hvc1) is (1) +[0x00004fe8] | IO | INFO | 2023-05-17 09:46:10,624 | Setting codec capacity (0) +[0x00004fe8] | SyManager | INFO | 2023-05-17 09:46:10,625 | Total: 20, NumDtThreads: 8, NumComms: 0, NumSites: 1 + +[0x00004fe8] | SyManager | INFO | 2023-05-17 09:46:10,625 | Lookaheads -> playback = 20, record = 20, stop = 2 + +[0x00004fe8] | DtManager | INFO | 2023-05-17 09:46:10,626 | Using 8 generic IO threads +[0x00004fe8] | DtManager | INFO | 2023-05-17 09:46:10,626 | Total of 16 IO threads (including 8 generic and 8 Red decode threads) +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:10,627 | Show splash screen message: Loading Project Libraries +[0x00001a20] | DtManager | INFO | 2023-05-17 09:46:10,627 | Dt Worker Thread Started +[0x00007da0] | GsManager | INFO | 2023-05-17 09:46:10,628 | Gs Processor Thread ----- (32160) + +[0x00004474] | DtManager | INFO | 2023-05-17 09:46:10,628 | Dt Worker Thread Started +[0x00002b7c] | DtManager | INFO | 2023-05-17 09:46:10,628 | Dt Worker Thread Started +[0x00002d64] | DtManager | INFO | 2023-05-17 09:46:10,628 | Dt Worker Thread Started +[0x00007bbc] | DtManager | INFO | 2023-05-17 09:46:10,628 | Dt Data Handler Thread Started +[0x00002ad0] | DtManager | INFO | 2023-05-17 09:46:10,628 | Dt Worker Thread Started +[0x00004a48] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started +[0x00005084] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started +[0x00003050] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started +[0x00003e88] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started +[0x00006550] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started +[0x00007950] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started +[0x000059a8] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started +[0x00007de4] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started +[0x00007a24] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started +[0x00001eec] | DtManager | INFO | 2023-05-17 09:46:10,630 | Dt Worker Thread Started +[0x00001f5c] | DtManager | INFO | 2023-05-17 09:46:10,630 | Dt Worker Thread Started +[0x00007a50] | DtManager | INFO | 2023-05-17 09:46:10,630 | Dt Worker Thread Started +[0x000009c4] | DtManager | INFO | 2023-05-17 09:46:10,630 | Dt Worker Thread Started +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:10,633 | Show splash screen message: Initializing system components +[0x00001190] | GPU.MultiBoardMgr | INFO | 2023-05-17 09:46:10,635 | Let There Be CUDA Light! +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:10,639 | Show splash screen message: Loading video codecs +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:10,644 | Show splash screen message: Loading video plugins +[0x00004fe8] | UI.GLContext | INFO | 2023-05-17 09:46:10,680 | Creating shared OpenGL context for this thread (1 total). +[0x00004fe8] | UI.GLContext | INFO | 2023-05-17 09:46:10,708 | Initialized OpenGL 4.6 (requested 2.0) on device 'NVIDIA Corporation NVIDIA GeForce RTX 2060 SUPER/PCIe/SSE2' +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:10,708 | Show splash screen message: Loading Fairlight Engine +[0x00001190] | GPU.MultiBoardMgr | INFO | 2023-05-17 09:46:10,708 | Initializing CUDA board manager for Main Display GPU gpu:a24a3d50.83f1fdb7. +[0x00001968] | IO | INFO | 2023-05-17 09:46:10,788 | IO codec initialization completed in 143 ms. +[0x000065b4] | GPU.SingleBoardMgr | INFO | 2023-05-17 09:46:10,811 | Board manager thread for "NVIDIA GeForce RTX 2060 SUPER" (gpu:a24a3d50.83f1fdb7) is ready. +[0x00001190] | UI.GLInterop | INFO | 2023-05-17 09:46:10,811 | OpenGL interop was initialized. +[0x00001190] | UI.GLInterop | INFO | 2023-05-17 09:46:10,811 | OpenGL interop was initialized. +[0x00001190] | GPU.MultiBoardMgr | INFO | 2023-05-17 09:46:10,811 | Enabled CUDA pinned memory. +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.122(003): WASAPI: Scanning Speakers (Steam Streaming Microphone) for formats +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.128(000): WASAPI: Device: Speakers (Steam Streaming Microphone). Scan time 6. Formats = 1 15 formats scanned +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.128(000): WASAPI: Scanning Speakers (High Definition Audio Device) for formats +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.190(000): WASAPI: Device: Speakers (High Definition Audio Device). Scan time 61. Formats = 1 15 formats scanned +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.190(000): WASAPI: Scanning Mi 27 NU (NVIDIA High Definition Audio) for formats +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.236(000): WASAPI: Device: Mi 27 NU (NVIDIA High Definition Audio). Scan time 45. Formats = 1 15 formats scanned +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.236(000): WASAPI: Scanning Speakers (Steam Streaming Speakers) for formats +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.242(000): WASAPI: Device: Speakers (Steam Streaming Speakers). Scan time 6. Formats = 1 15 formats scanned +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.243(001): WASAPI: Scanning Digital Audio (S/PDIF) (High Definition Audio Device) for formats +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,212 | 00.00.00.286(000): WASAPI: Device: Digital Audio (S/PDIF) (High Definition Audio Device). Scan time 42. Formats = 1 15 formats scanned +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,212 | 00.00.00.287(001): WASAPI: Scanning Microphone (Steam Streaming Microphone) for formats +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,212 | 00.00.00.294(000): WASAPI: Device: Microphone (Steam Streaming Microphone). Scan time 7. Formats = 0 16 formats scanned +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,212 | 00.00.00.390(002): WASAPI: switching to AUTO because exclusive mode not allowed +[0x00004f94] | DbCommon2 | ERROR | 2023-05-17 09:46:11,307 | Cannot connect to soundfx192.168.100.31 database: could not connect to server: Connection refused (0x0000274D/10061) + Is the server running on host "192.168.100.31" and accepting + TCP/IP connections on port 5432? +QPSQL: Unable to connect +[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,339 | postgres project library homepc at 127.0.0.1 version 9.5.22 +[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,347 | Project library [homepc127.0.0.1] current version <18.1.0.004> updated on <2022-11-22T16:16:24.518>, remark: +[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,347 | Connect to postgres project library homepc127.0.0.1 +[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,396 | postgres project library barney at 192.168.100.30 version 9.5.25 +[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,422 | Project library [barney192.168.100.30] current version <18.1.0.004> updated on <2022-11-17T08:47:18.322>, remark: +[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,422 | Connect to postgres project library barney192.168.100.30 +[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,475 | postgres project library shtv_barney at 192.168.100.30 version 9.5.25 +[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,501 | Project library [shtv_barney192.168.100.30] current version <18.1.0.004> updated on <2022-11-15T13:16:50.006>, remark: +[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,501 | Connect to postgres project library shtv_barney192.168.100.30 +[0x00005850] | Fusion | INFO | 2023-05-17 09:46:11,704 | 260 templates scanned in 0.12 secs +[0x00004fe8] | Fairlight | INFO | 2023-05-17 09:46:12,310 | 00.00.01.565(000): Running Fairlight (939d5c56aa8d66c1f392ff963f9b9c349ef4d9fb) +[0x00004fe8] | FairlightLoader | INFO | 2023-05-17 09:46:12,310 | Fairlight lib initialized in 1598 ms. +[0x00006a7c] | UI.GLContext | INFO | 2023-05-17 09:46:12,405 | Creating shared OpenGL context for this thread (2 total). +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:12,417 | 00.00.01.501(002): WASAPI: switching to AUTO because exclusive mode not allowed +[0x00006a7c] | UI.GLIO | INFO | 2023-05-17 09:46:12,454 | Initialized MainPlayer OpenGL I/O on CUDA device 'NVIDIA GeForce RTX 2060 SUPER' +[0x00004fe8] | UI.GLIO | INFO | 2023-05-17 09:46:12,454 | MainPlayer: OpenGL I/O setup done. +[0x000076bc] | UI.GLContext | INFO | 2023-05-17 09:46:12,455 | Creating shared OpenGL context for this thread (3 total). +[0x000076bc] | UI.GLIO | INFO | 2023-05-17 09:46:12,505 | Initialized AuxPlayer OpenGL I/O on CUDA device 'NVIDIA GeForce RTX 2060 SUPER' +[0x00004fe8] | UI.GLIO | INFO | 2023-05-17 09:46:12,505 | AuxPlayer: OpenGL I/O setup done. +[0x00008190] | UI.GLContext | INFO | 2023-05-17 09:46:12,506 | Creating shared OpenGL context for this thread (4 total). +[0x00008190] | UI.Scopes | INFO | 2023-05-17 09:46:12,565 | Initialized GPU Scopes Manager on CUDA device 'NVIDIA GeForce RTX 2060 SUPER' +[0x00004fe8] | UI.MenuBar | WARN | 2023-05-17 09:46:12,649 | Main menu action [workspaceLayoutFusion_sub001Default]'s slot is not defined: workspaceLayoutFusion_sub001Default_triggered() +[0x00004fe8] | UI.MenuBar | WARN | 2023-05-17 09:46:12,649 | Main menu action [workspaceWIPlugins_placeholder]'s slot is not defined: workspaceWIPlugins_placeholder_triggered() +[0x00006e14] | SyManager | WARN | 2023-05-17 09:46:12,656 | socket failed to connect to server, error: 10061 + +[0x00006e14] | SyManager | ERROR | 2023-05-17 09:46:12,656 | DRIVER: panel connection failed +[0x00004fe8] | FusionUtils | WARN | 2023-05-17 09:46:12,657 | Fusion DoAction ACTION_SET_FUSION_HOTKEY ignored, init not completed +[0x000052e4] | BtCommon | INFO | 2023-05-17 09:46:12,661 | Starting Daemon: C:/Program Files/Blackmagic Design/DaVinci Resolve/DaVinciPanelDaemon.exe +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:12,686 | Show splash screen message: Loading Project Settings +[0x00006e14] | SyManager | INFO | 2023-05-17 09:46:12,757 | Connection to the panel server has been re-established +[0x00004fe8] | UI | WARN | 2023-05-17 09:46:12,899 | Failed to find value '0' in combo-box +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:19,055 | Show splash screen message: Loading Media Page +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:46:19,145 | Action [fairlightBusStructure] is not a global action +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:46:19,145 | Action [fairlightBusStructure] is not a valid global action +[0x00004fe8] | UI.FairlightInterface | WARN | 2023-05-17 09:46:19,269 | SetActionEnabled: Failed to find action [viewTimelineScrollingFixed]'s action connector for handler Id [1] +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:19,424 | Show splash screen message: Loading Cut Page +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:19,698 | Show splash screen message: Loading Edit Page +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:19,951 | Show splash screen message: Loading Fusion Page +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:20,038 | Show splash screen message: Loading Fairlight Page +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:20,227 | Show splash screen message: Loading Color Page +[0x00004fe8] | UI | INFO | 2023-05-17 09:46:20,296 | Not creating special GL widget for screen 0 +[0x00004fe8] | UI | WARN | 2023-05-17 09:46:21,003 | Failed to find value '8192' in combo-box +[0x00004fe8] | UI | WARN | 2023-05-17 09:46:21,284 | Failed to find value '100' in combo-box +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:22,006 | Show splash screen message: Loading Waveform Monitor +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:22,433 | Show splash screen message: Loading Audio Plugins +[0x00004fe8] | SyManager | INFO | 2023-05-17 09:46:22,528 | Collaboration IP 127.0.0.1 Port 0 +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:22,674 | Show splash screen message: Loading Projects +[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:46:22,739 | Current user pointer is changed +[0x00004fe8] | UI | WARN | 2023-05-17 09:46:22,755 | UI Persistence - SecondaryScreenIdx not read +[0x00004fe8] | UI | WARN | 2023-05-17 09:46:22,755 | UI Persistence - UseDisplayNameForClips not read +[0x00004fe8] | UI | WARN | 2023-05-17 09:46:22,755 | UI Persistence - DefaultTransitionKey not read +[0x00004fe8] | UI | WARN | 2023-05-17 09:46:22,755 | UI Persistence - DefaultAudioTransitionKey not read +[0x00004fe8] | IO | INFO | 2023-05-17 09:46:22,767 | RED rocket decode has been disabled in the config file +[0x00004fe8] | LeManager | ERROR | 2023-05-17 09:46:22,820 | 444, 139, 0 +[0x00004fe8] | SyManager.DeckLink | ERROR | 2023-05-17 09:46:22,922 | Failed to create instance. +[0x00004fe8] | SyManager | INFO | 2023-05-17 09:46:22,973 | Search Parameter (-2) or Search Operation (0) not supported, and hence disabling the condition. +[0x00004fe8] | SyManager | INFO | 2023-05-17 09:46:22,973 | Search Parameter (-1) or Search Operation (-1) not supported, and hence disabling the condition. +[0x00004fe8] | UI | WARN | 2023-05-17 09:46:23,077 | Failed to find value '0' in combo-box +[0x00004fe8] | UI | WARN | 2023-05-17 09:46:23,080 | Failed to find value '0' in combo-box +[0x00004fe8] | UI | WARN | 2023-05-17 09:46:23,098 | Failed to find value '0' in combo-box +[0x00004fe8] | UI | WARN | 2023-05-17 09:46:23,099 | Failed to find value '4' in combo-box +[0x00004fe8] | SyManager.Gallery | INFO | 2023-05-17 09:46:23,119 | Start purging still caches +[0x00004fe8] | SyManager.Gallery | INFO | 2023-05-17 09:46:23,119 | Finish purging still caches +[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:46:23,149 | Media pool relink status changed to 0 +[0x00004fe8] | FusionUtils | WARN | 2023-05-17 09:46:23,167 | Fusion DoAction ACTION_SET_FUSION_HOTKEY ignored, init not completed +[0x00004fe8] | UI | INFO | 2023-05-17 09:46:23,179 | Launching project manager +[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:46:23,182 | Main view page is changed to 12 +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:23,209 | Show splash screen message: Ready +[0x00004fe8] | UI | INFO | 2023-05-17 09:46:23,214 | Gallery pointer is changed, refreshing color gallery +[0x00004fe8] | UI | INFO | 2023-05-17 09:46:23,223 | Gallery pointer is changed, refreshing gallery browser +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:23,312 | Close splash screen +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:23,314 | Launching main loop +[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:46:24,215 | Fusion templates changed +[0x00004fe8] | Fusion | INFO | 2023-05-17 09:46:24,758 | Started script server: 32648 + +[0x00004fe8] | SyManager.ActionExecutor | INFO | 2023-05-17 09:48:41,800 | Database transaction is ongoing, user initiated action Sync Asset Map is postponed +[0x00004fe8] | SyManager.ActionExecutor | INFO | 2023-05-17 09:48:41,800 | Database transaction completed, enqueueing 1 postponed actions +[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:48:41,842 | Loading project (gazprom_screens_06_R_Home_Painter_Lookdev_v002) from project library (homepc127.0.0.1) took 291 ms +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:42,159 | Action [fairlightBusStructure] is not a global action +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:48:42,390 | 00.02.31.644(002): WASAPI: switching to AUTO because exclusive mode not allowed +[0x00004fe8] | UI | WARN | 2023-05-17 09:48:43,528 | Failed to find value '0' in combo-box +[0x00004fe8] | UI | WARN | 2023-05-17 09:48:43,531 | Failed to find value '0' in combo-box +[0x00004fe8] | SyManager.Gallery | INFO | 2023-05-17 09:48:43,539 | Start purging still caches +[0x00004fe8] | SyManager.Gallery | INFO | 2023-05-17 09:48:43,539 | Finish purging still caches +[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:48:43,557 | Media pool relink status changed to 0 +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:48:43,596 | 00.02.32.735(002): WASAPI: switching to AUTO because exclusive mode not allowed +[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:48:43,862 | Current project pointer (gazprom_screens_06_R_Home_Painter_Lookdev_v002) is changed +[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:48:43,862 | Current timeline pointer () is changed +[0x00004fe8] | UI | INFO | 2023-05-17 09:48:43,868 | Lock project 513e336f-59c3-4fa8-b9e9-814cf2b797a3 +[0x00004fe8] | Undefined | INFO | 2023-05-17 09:48:43,868 | The buttonId [7] is not found for [0] in [0] +[0x00004fe8] | UI | INFO | 2023-05-17 09:48:43,876 | UiGPU::UploadWipeData called before UiGPU has been initialized. Returning false +[0x00004fe8] | UI | INFO | 2023-05-17 09:48:43,877 | Gallery pointer is changed, refreshing color gallery +[0x00004fe8] | UI | INFO | 2023-05-17 09:48:43,891 | Gallery pointer is changed, refreshing gallery browser +[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:48:43,921 | Main view page is changed to 1 +[0x00004fe8] | UI | WARN | 2023-05-17 09:48:43,921 | Failed to get auto update information. +[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:48:43,930 | Main view page is changed to 1 +[0x00004fe8] | UI | WARN | 2023-05-17 09:48:44,566 | UI Persistence - MediaPoolFloatingWindowGeometry not read +[0x00004fe8] | Undefined | INFO | 2023-05-17 09:48:44,583 | The buttonId [7] is not found for [0] in [0] +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:48:44,602 | 00.02.33.795(001): WASAPI: switching to AUTO because exclusive mode not allowed +[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:48:44,912 | Main view page is changed to 1 +[0x00004fe8] | UI | INFO | 2023-05-17 09:48:45,076 | Launching main window +[0x00004fe8] | SyManager | WARN | 2023-05-17 09:48:45,094 | No reply received from file system, assume successfully deleted folder D:\SYNC\BACKUP\Resolve Project Backups\64d8dbe8-256e-46b1-81da-671a6a8fc423. +[0x00004fe8] | SyManager | WARN | 2023-05-17 09:48:45,105 | No reply received from file system, assume successfully deleted folder C:\Users\videopro\Videos\CacheClip\64d8dbe8-256e-46b1-81da-671a6a8fc423. +[0x00004fe8] | SyManager | WARN | 2023-05-17 09:48:45,116 | No reply received from file system, assume successfully deleted folder C:\Users\videopro\Videos\.gallery\64d8dbe8-256e-46b1-81da-671a6a8fc423. +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,122 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_newProject] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,122 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_newFolder] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,122 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_openProject] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,122 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_openProjectInReadOnlyMode] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,122 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_Close] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,122 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_Rename] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,122 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_saveProject] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,122 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_saveProjectAs] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_importProject] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_importProjectPlus] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_exportProject] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_exportProjectWithStillsAndLuts] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_restoreProject] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_restoreProjectPlus] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_archiveProject] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_unlockProject] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [editDelete] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [editCut] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [editCopy] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [editPaste] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_loadProjectSettingsToCurrentProject] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_projectBackups] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_projectSettings] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_dynamicProjectSwitching] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_closeProjectsInMemory] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_updateThumbnails] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_otherProjectBackups] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_refresh] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_resetUiLayout] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:51,277 | Action owner [MediaPool] is not active when refreshing shortcut [editDuplicateTimelineOrClips] due to [No focused widget is set yet] +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:01,060 | Traceback (most recent call last): +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:01,060 | File "", line 1, in +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:01,061 | AttributeError +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:01,061 | : +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:01,061 | module 'sys' has no attribute '__path__' +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:01,061 | +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:08,715 | Traceback (most recent call last): +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:08,715 | File "", line 1, in +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:08,715 | AttributeError +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:08,716 | : +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:08,716 | module 're' has no attribute '__path__' +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:08,716 | +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:21,531 | Traceback (most recent call last): +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:21,531 | File "", line 1, in +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:21,532 | TypeError +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:21,532 | : +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:21,532 | split() missing 2 required positional arguments: 'pattern' and 'string' +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:21,532 | +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:29,443 | Traceback (most recent call last): +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:29,443 | File "", line 1, in +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:29,444 | NameError +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:29,444 | : +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:29,444 | name 're__path__' is not defined +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:29,444 | +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:33,563 | Traceback (most recent call last): +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:33,563 | File "", line 1, in +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:33,564 | AttributeError +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:33,564 | : +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:33,564 | module 're' has no attribute '__path__' +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:33,564 | +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:39,740 | Traceback (most recent call last): +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:39,740 | File "", line 1, in +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:39,740 | AttributeError +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:39,740 | : +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:39,740 | module 'sys' has no attribute 'info' +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:39,740 | +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:41,372 | Traceback (most recent call last): +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:41,372 | File "", line 1, in +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:41,372 | AttributeError +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:41,372 | : +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:41,372 | module 'sys' has no attribute 'info' +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:41,372 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:50:52,078 | >>> [ openpype.hosts.resolve installed ] +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:50:52,083 | >>> [ Registering DaVinci Resovle plug-ins.. ] +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:50:52,100 | >>> [ Assigning resolve module to `pype.hosts.resolve.api.bmdvr`: Resolve (0x00007FF6BB5340D0) [App: 'Resolve' on 127.0.0.1, UUID: f0d29a26-23ed-495a-9eb5-f264f7187252] ] +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:50:52,105 | >>> [ Assigning resolve module to `pype.hosts.resolve.api.bmdvf`: FusionUI (0x0000023A629E7040) [App: 'Resolve' on 127.0.0.1, UUID: f0d29a26-23ed-495a-9eb5-f264f7187252] ] +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:50:52,110 | - { timers_manager }: [ Installing task changed callback ] +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:51:12,816 | - { openpype.pipeline.anatomy }: [ Looking for matching root in path "D:/SYNC/OPENPYPE/gazprom_screens/06_R_Home_Painter/work/Lookdev". ] +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:51:12,821 | >>> [ Found match in root "work". ] +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,952 | Traceback (most recent call last): +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,953 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\subsetmanager\window.py", line 190, in showEvent +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,953 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,953 | self.refresh() +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,953 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,953 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\subsetmanager\window.py", line 176, in refresh +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,953 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,954 | self._model.refresh() +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,954 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,954 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\subsetmanager\model.py", line 27, in refresh +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,954 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,954 | instances = list_instances() +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,954 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,954 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\hosts\resolve\api\pipeline.py", line 285, in list_instances +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,955 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,955 | selected_timeline_items = lib.get_current_timeline_items( +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,955 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,955 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\hosts\resolve\api\lib.py", line 319, in get_current_timeline_items +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,955 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,955 | selected_track_count = timeline.GetTrackCount(track_type) +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,955 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,956 | AttributeError +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,956 | : +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,956 | 'NoneType' object has no attribute 'GetTrackCount' +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,956 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,000 | Traceback (most recent call last): +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,000 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\libraryloader\app.py", line 376, in _assetschanged +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,000 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,000 | subsets_model.set_assets(asset_ids) +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,001 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,001 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\loader\model.py", line 259, in set_assets +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,001 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,001 | self.refresh() +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,001 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,001 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\loader\model.py", line 577, in refresh +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,001 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,002 | repre_ids = {con.get("representation") for con in containers} +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,002 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,002 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\loader\model.py", line 577, in +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,002 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,002 | repre_ids = {con.get("representation") for con in containers} +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,002 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,002 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\hosts\resolve\api\pipeline.py", line 138, in ls +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,003 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,003 | all_timeline_items = lib.get_current_timeline_items(filter=False) +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,003 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,003 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\hosts\resolve\api\lib.py", line 319, in get_current_timeline_items +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,003 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,003 | selected_track_count = timeline.GetTrackCount(track_type) +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,003 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,003 | AttributeError +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,004 | : +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,004 | 'NoneType' object has no attribute 'GetTrackCount' +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,004 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,731 | Traceback (most recent call last): +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,732 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\libraryloader\app.py", line 376, in _assetschanged +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,732 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,732 | subsets_model.set_assets(asset_ids) +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,732 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,732 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\loader\model.py", line 259, in set_assets +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,732 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,733 | self.refresh() +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,733 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,733 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\loader\model.py", line 577, in refresh +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,733 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,733 | repre_ids = {con.get("representation") for con in containers} +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,733 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,734 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\loader\model.py", line 577, in +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,734 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,734 | repre_ids = {con.get("representation") for con in containers} +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,734 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,734 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\hosts\resolve\api\pipeline.py", line 138, in ls +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,734 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,734 | all_timeline_items = lib.get_current_timeline_items(filter=False) +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,735 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,735 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\hosts\resolve\api\lib.py", line 319, in get_current_timeline_items +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,735 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,735 | selected_track_count = timeline.GetTrackCount(track_type) +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,735 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,735 | AttributeError +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,735 | : +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,735 | 'NoneType' object has no attribute 'GetTrackCount' +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,736 | +[0x00004fe8] | UI | WARN | 2023-05-17 09:53:56,329 | UI Persistence - MediaPoolFloatingWindowGeometry not read +[0x00004fe8] | UI | WARN | 2023-05-17 09:53:56,329 | UI Persistence - ConformEdlEffectsLibrary not read +[0x000065b4] | GPU.SingleBoardMgr | INFO | 2023-05-17 09:53:56,766 | Flushing GPU memory... +[0x000065b4] | UI.GLContext | INFO | 2023-05-17 09:53:56,766 | Creating shared OpenGL context for this thread (5 total). +[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:53:56,782 | Main view page is changed to 2 +[0x000065b4] | UI.GLTexPool | INFO | 2023-05-17 09:53:56,884 | Released 0 MiB in 0 unused textures. +[0x00004fe8] | UI | INFO | 2023-05-17 09:53:56,944 | PBO is initialized with size [1920x1080], bitDepth=[8], hasAlpha=[1]. +[0x00004fe8] | UI | WARN | 2023-05-17 09:53:59,491 | Failed to find value '1' in combo-box +[0x00004fe8] | UI | WARN | 2023-05-17 09:53:59,491 | Failed to find value '2' in combo-box +[0x00004fe8] | UI | WARN | 2023-05-17 09:53:59,491 | Failed to find value '2' in combo-box +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:53:59,548 | Action owner [MediaPool] is not active when refreshing shortcut [editDuplicateTimelineOrClips] due to [No focused widget is set yet] +[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:54:01,297 | Current timeline pointer (Timeline 1) is changed +[0x00004fe8] | UI | ERROR | 2023-05-17 09:54:01,405 | No marker found at frame -90000 to delete. +[0x00004fe8] | UI | ERROR | 2023-05-17 09:54:01,405 | No marker found at frame -90000 to delete. +[0x00004fe8] | UI | ERROR | 2023-05-17 09:54:01,405 | No marker found at frame -90000 to delete. +[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:01,495 | Start saving project gazprom_screens_06_R_Home_Painter_Lookdev_v002 +[0x000083dc] | SyManager.ActionExecutor | INFO | 2023-05-17 09:54:01,526 | Database transaction is ongoing, user initiated action SmActIOCPSigInvoker is postponed +[0x000083dc] | SyManager.ActionExecutor | INFO | 2023-05-17 09:54:01,550 | Database transaction is ongoing, user initiated action SmActIOCPSigInvoker is postponed +[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:01,592 | Saving project took 97 ms +[0x00004fe8] | SyManager.ActionExecutor | INFO | 2023-05-17 09:54:01,619 | Database transaction completed, enqueueing 2 postponed actions +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:54:23,357 | >>> [ openpype.hosts.resolve installed ] +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:54:23,363 | >>> [ Registering DaVinci Resovle plug-ins.. ] +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:54:23,375 | >>> [ Assigning resolve module to `pype.hosts.resolve.api.bmdvr`: Resolve (0x00007FF6BB5340D0) [App: 'Resolve' on 127.0.0.1, UUID: f0d29a26-23ed-495a-9eb5-f264f7187252] ] +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:54:23,380 | >>> [ Assigning resolve module to `pype.hosts.resolve.api.bmdvf`: FusionUI (0x0000023A629E7040) [App: 'Resolve' on 127.0.0.1, UUID: f0d29a26-23ed-495a-9eb5-f264f7187252] ] +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:54:23,386 | - { timers_manager }: [ Installing task changed callback ] +[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:39,823 | Start saving project gazprom_screens_06_R_Home_Painter_Lookdev_v002 +[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:39,826 | Saving project took 2 ms +[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:39,920 | Start saving project gazprom_screens_06_R_Home_Painter_Lookdev_v002 +[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:39,923 | Saving project took 3 ms +[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:40,137 | Start saving project gazprom_screens_06_R_Home_Painter_Lookdev_v002 +[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:40,321 | Saving project took 183 ms +[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:54:40,326 | Main view page is changed to 2 +[0x00004fe8] | BtCommon | WARN | 2023-05-17 09:54:40,570 | Negative duration +[0x00004fe8] | UI | WARN | 2023-05-17 09:54:40,638 | Unable to submit frame to GPU scopes, legacy OpenGL uploads not supported (Player Model). +[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:42,497 | Start saving project gazprom_screens_06_R_Home_Painter_Lookdev_v002 +[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:42,555 | Saving project took 59 ms diff --git a/openpype/hosts/resolve/hooks/pre_resolve_setup.py b/openpype/hosts/resolve/hooks/pre_resolve_setup.py index 8c88478104..486d8121cf 100644 --- a/openpype/hosts/resolve/hooks/pre_resolve_setup.py +++ b/openpype/hosts/resolve/hooks/pre_resolve_setup.py @@ -33,6 +33,9 @@ class ResolvePrelaunch(PreLaunchHook): resolve_script_api = Path( resolve_script_api_locations[current_platform] ) + self.log.info( + f"setting RESOLVE_SCRIPT_API variable to {resolve_script_api}" + ) self.launch_context.env[ "RESOLVE_SCRIPT_API" ] = resolve_script_api.as_posix() @@ -52,6 +55,9 @@ class ResolvePrelaunch(PreLaunchHook): self.launch_context.env[ "RESOLVE_SCRIPT_LIB" ] = resolve_script_lib.as_posix() + self.log.info( + f"setting RESOLVE_SCRIPT_LIB variable to {resolve_script_lib}" + ) # TODO: add OTIO installation from `openpype/requirements.py` # making sure python <3.9.* is installed at provided path @@ -69,14 +75,6 @@ class ResolvePrelaunch(PreLaunchHook): self.launch_context.env["PYTHONHOME"] = python3_home_str self.log.info(f"Path to Resolve Python folder: `{python3_home_str}`") - # add to the python path to PATH - env_path = self.launch_context.env["PATH"] - self.launch_context.env[ - "PATH" - ] = f"{python3_home_str}{os.pathsep}{env_path}" - - self.log.debug(f"PATH: {self.launch_context.env['PATH']}") - # add to the PYTHONPATH env_pythonpath = self.launch_context.env["PYTHONPATH"] modules_path = Path(resolve_script_api, "Modules").as_posix() From cd7b1d92f8169ac655f69cd1ba69272e449fa19b Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov Date: Wed, 17 May 2023 10:05:28 +0300 Subject: [PATCH 060/189] remove resolve logs --- .../Support/logs/davinci_resolve.log | 443 ------------------ 1 file changed, 443 deletions(-) delete mode 100644 igniter/#ProgramData#Blackmagic Design/DaVinci Resolve/Support/logs/davinci_resolve.log diff --git a/igniter/#ProgramData#Blackmagic Design/DaVinci Resolve/Support/logs/davinci_resolve.log b/igniter/#ProgramData#Blackmagic Design/DaVinci Resolve/Support/logs/davinci_resolve.log deleted file mode 100644 index 8f1a3b712e..0000000000 --- a/igniter/#ProgramData#Blackmagic Design/DaVinci Resolve/Support/logs/davinci_resolve.log +++ /dev/null @@ -1,443 +0,0 @@ -[0x00004fe8] | Undefined | INFO | 2023-05-17 09:46:08,249 | -------------------------------------------------------------------------------- -[0x00004fe8] | Undefined | INFO | 2023-05-17 09:46:08,250 | Loaded log config from C:\Users\videopro\AppData\Roaming\Blackmagic Design\DaVinci Resolve\Preferences\log-conf.xml -[0x00004fe8] | Undefined | INFO | 2023-05-17 09:46:08,250 | -------------------------------------------------------------------------------- -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:08,251 | Running DaVinci Resolve Studio v18.1.2.0006 (Windows/MSVC x86_64) -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:08,251 | BMD_BUILD_UUID 3ff36663-26c9-45d9-8506-101676c881e0 -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:08,251 | BMD_GIT_COMMIT a3be29d0542aeafcb1b0933bb5ace426aa7d047d -[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,259 | Starting GPUDetect 1.2_3-a1 -[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | Done in 161 ms. -[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | Detected System: -[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | - OS: Windows 10 Pro (Build 19045) -[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | - Model: ASUSTeK ROG STRIX B550-F GAMING (WI-FI) -[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | - System ID: 838d5045-58b0-44ed-854c-19be5b814d6f -[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | - CPU: AMD Ryzen 7 3700X, 16 threads, x86-64 -[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | - RAM: 16.6 GiB used of 63.9 GiB -[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | - NVIDIA GPU Driver: 527.56, supports CUDA 12.0 -[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | Detected 1 GPUs: -[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,423 | - "NVIDIA GeForce RTX 2060 SUPER" (gpu:a24a3d50.83f1fdb7) <- Main Display GPU -[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,423 | Discrete, 2.0 GiB used of 7.6 GiB VRAM, PCI:8:0 (x16) -[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,423 | Matches: CUDA, DirectX, NVAPI, NVML, OpenCL, Win32 -[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,423 | Detected 1 monitors: -[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,423 | - "Generic PnP Monitor" <- Main Monitor -[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,423 | 3840x2160, connected to "NVIDIA GeForce RTX 2060 SUPER" (gpu:a24a3d50.83f1fdb7) -[0x00004fe8] | Main.GPUConfig | INFO | 2023-05-17 09:46:08,424 | Selected compute API: CUDA -[0x00004fe8] | Main.GPUConfig | INFO | 2023-05-17 09:46:08,424 | Automatic GPU Selection: -[0x00004fe8] | Main.GPUConfig | INFO | 2023-05-17 09:46:08,424 | - "NVIDIA GeForce RTX 2060 SUPER" (gpu:a24a3d50.83f1fdb7) -[0x00004fe8] | IO | INFO | 2023-05-17 09:46:08,438 | RED InitializeSdk with library path at C:\ProgramData\Blackmagic Design\DaVinci Resolve\Support\Libraries -[0x00004fe8] | IO | INFO | 2023-05-17 09:46:08,642 | R3DAPI 8.3.1-52407 (20220725 Wx64S) R3DSDK 8.3.1-52407 (20220725 Wx64D C3B3) RED CUDA 8.3.1-52408 (20220725) [C:\ProgramData\Blackmagic Design\DaVinci Resolve\Support\Libraries\] init is successful -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:08,642 | 0 RED rocket cards available -[0x00004fe8] | SyManager.DeckLink | ERROR | 2023-05-17 09:46:08,645 | Failed to create instance. -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:08,645 | Decklink model name: '', version: '' -[0x00004fe8] | DVIP | INFO | 2023-05-17 09:46:08,645 | DVIP release/18.1.2 build 2 (86acfdf407856b6cd8daf2517ab0b44f1efc332f). Release, version 18.1.2. -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:08,645 | Resolve Module Handle 00007FF6A4B50000 -[0x00001238] | IO | INFO | 2023-05-17 09:46:08,646 | Using DNxHR library v2.7.3.27r -[0x00002044] | Fusion | INFO | 2023-05-17 09:46:09,093 | Fusion Build: 008b13c7_0004 (Dec 7 2022 15:23:40) -[0x00005f70] | Fusion | INFO | 2023-05-17 09:46:09,114 | fusionsystem: = "C:\Program Files\Blackmagic Design\DaVinci Resolve\fusionsystem.dll" -[0x00001238] | IO | INFO | 2023-05-17 09:46:09,117 | NVDEC is using upto (1023) MB -[0x00001238] | IO | INFO | 2023-05-17 09:46:09,119 | NVDEC decodes H264, chroma 4:2:0, bitdepth 8, upto 4096 x 4096 -[0x00001238] | IO | INFO | 2023-05-17 09:46:09,121 | NVDEC decodes HEVC, chroma 4:2:0, bitdepth 8, upto 8192 x 8192 -[0x00001238] | IO | INFO | 2023-05-17 09:46:09,123 | NVDEC decodes HEVC, chroma 4:2:0, bitdepth 10, upto 8192 x 8192 -[0x00001238] | IO | INFO | 2023-05-17 09:46:09,127 | NVDEC decodes HEVC, chroma 4:2:0, bitdepth 12, upto 8192 x 8192 -[0x00001238] | IO | INFO | 2023-05-17 09:46:09,128 | NVDEC decodes HEVC, chroma 4:4:4, bitdepth 8, upto 8192 x 8192 -[0x00001238] | IO | INFO | 2023-05-17 09:46:09,130 | NVDEC decodes HEVC, chroma 4:4:4, bitdepth 10, upto 8192 x 8192 -[0x00001238] | IO | INFO | 2023-05-17 09:46:09,131 | NVDEC decodes HEVC, chroma 4:4:4, bitdepth 12, upto 8192 x 8192 -[0x00001238] | IO | INFO | 2023-05-17 09:46:09,133 | NVDEC decodes VP9, chroma 4:2:0, bitdepth 8, upto 8192 x 8192 -[0x00001238] | IO | INFO | 2023-05-17 09:46:09,134 | NVDEC decodes VP9, chroma 4:2:0, bitdepth 10, upto 8192 x 8192 -[0x00005f70] | Fusion | INFO | 2023-05-17 09:46:09,148 | FusionLibs: = "C:\Program Files\Blackmagic Design\DaVinci Resolve\" -[0x00005f70] | Fusion | INFO | 2023-05-17 09:46:09,148 | UserData: = "C:\Users\videopro\AppData\Roaming\Blackmagic Design\DaVinci Resolve\Support\Fusion" -[0x00005f70] | Fusion | INFO | 2023-05-17 09:46:09,148 | Profiles: = "UserData:Profiles\" -[0x00001238] | IO | INFO | 2023-05-17 09:46:09,148 | Nvidia GPU (0) is initialised as decoding and encoding device. -[0x00001238] | IO | INFO | 2023-05-17 09:46:09,182 | IO codec library load completed in 536 ms. -[0x00005cbc] | SyManager | ERROR | 2023-05-17 09:46:09,202 | BlackmagicIDHelper::GetProjectLibraries() - Access token is empty -[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:09,268 | Loading dblist file: C:\Users\videopro\AppData\Roaming\Blackmagic Design\DaVinci Resolve\Preferences\dblist.conf -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:09,314 | Finished loading Application style sheet -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:09,396 | Show splash screen -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:09,396 | Show splash screen message: Starting Up -[0x00004fe8] | Fusion | INFO | 2023-05-17 09:46:09,398 | Module Handle 0000023A5FC80000 fusionsystem -[0x0000204c] | Fusion | INFO | 2023-05-17 09:46:09,410 | Module Handle 0000023A6C160000 C:\Program Files\Blackmagic Design\DaVinci Resolve\fusiongraphics.dll -[0x00004f28] | Fusion | INFO | 2023-05-17 09:46:09,437 | Module Handle 0000023A6FB90000 fusionoperators.dll -[0x00005f70] | Fusion | INFO | 2023-05-17 09:46:09,462 | Module Handle 0000023A70490000 fusioncontrols.dll -[0x00007b34] | Fusion | INFO | 2023-05-17 09:46:09,488 | Module Handle 0000023A70870000 3d.plugin -[0x0000772c] | Fusion | INFO | 2023-05-17 09:46:09,510 | Module Handle 0000023A70B50000 dimension.plugin -[0x00003934] | Fusion | INFO | 2023-05-17 09:46:09,528 | Module Handle 0000023A70F70000 alembic.plugin -[0x0000204c] | Fusion | INFO | 2023-05-17 09:46:09,551 | Module Handle 0000023A71320000 fbx.plugin -[0x000078cc] | Fusion | INFO | 2023-05-17 09:46:09,566 | Module Handle 0000023A464C0000 fuses.plugin -[0x00004f28] | Fusion | INFO | 2023-05-17 09:46:09,582 | Module Handle 0000023A71A70000 opencolorio.plugin -[0x00005f70] | Fusion | INFO | 2023-05-17 09:46:09,599 | Module Handle 0000023A6ADF0000 openfx.plugin -[0x00007b34] | Fusion | INFO | 2023-05-17 09:46:09,615 | Module Handle 0000023A68DA0000 openvr.plugin -[0x0000772c] | Fusion | INFO | 2023-05-17 09:46:09,632 | Module Handle 0000023A6AE60000 paint.plugin -[0x00003934] | Fusion | INFO | 2023-05-17 09:46:09,653 | Module Handle 0000023A6AED0000 particles.plugin -[0x0000204c] | Fusion | INFO | 2023-05-17 09:46:09,673 | Module Handle 0000023A720D0000 text.plugin -[0x000078cc] | Fusion | INFO | 2023-05-17 09:46:09,690 | Module Handle 0000023A6CB80000 utilities.plugin -[0x00004f28] | Fusion | INFO | 2023-05-17 09:46:09,715 | Module Handle 00007FF89F590000 KrokodoveFu16.plugin -[0x00005f70] | Fusion | INFO | 2023-05-17 09:46:09,717 | Module Handle 00007FF89F330000 KrokodoveFu17.plugin -[0x00004f28] | OpenFX | INFO | 2023-05-17 09:46:09,817 | No context is available for com.absoft.NeatVideo5 -[0x00007fc8] | Main | INFO | 2023-05-17 09:46:09,828 | Started listener socket at port 15000 -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:10,308 | Show splash screen message: Checking Licenses -[0x00004fe8] | BtCommon | INFO | 2023-05-17 09:46:10,532 | Memory config: reserved=12270M pinned=8000M log=0 -[0x00004fe8] | BtCommon | INFO | 2023-05-17 09:46:10,532 | Using default pooled memory manager -[0x00003798] | LeManager | INFO | 2023-05-17 09:46:10,533 | 521, 29 -[0x00008180] | BtCommon | INFO | 2023-05-17 09:46:10,533 | BtResourceManager Process Thread Started -[0x00004fe8] | IO | INFO | 2023-05-17 09:46:10,609 | WMF encoder cnt for SW (hvc1) is (0) -[0x00004fe8] | IO | INFO | 2023-05-17 09:46:10,624 | WMF encoder cnt for HW (hvc1) is (1) -[0x00004fe8] | IO | INFO | 2023-05-17 09:46:10,624 | Setting codec capacity (0) -[0x00004fe8] | SyManager | INFO | 2023-05-17 09:46:10,625 | Total: 20, NumDtThreads: 8, NumComms: 0, NumSites: 1 - -[0x00004fe8] | SyManager | INFO | 2023-05-17 09:46:10,625 | Lookaheads -> playback = 20, record = 20, stop = 2 - -[0x00004fe8] | DtManager | INFO | 2023-05-17 09:46:10,626 | Using 8 generic IO threads -[0x00004fe8] | DtManager | INFO | 2023-05-17 09:46:10,626 | Total of 16 IO threads (including 8 generic and 8 Red decode threads) -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:10,627 | Show splash screen message: Loading Project Libraries -[0x00001a20] | DtManager | INFO | 2023-05-17 09:46:10,627 | Dt Worker Thread Started -[0x00007da0] | GsManager | INFO | 2023-05-17 09:46:10,628 | Gs Processor Thread ----- (32160) - -[0x00004474] | DtManager | INFO | 2023-05-17 09:46:10,628 | Dt Worker Thread Started -[0x00002b7c] | DtManager | INFO | 2023-05-17 09:46:10,628 | Dt Worker Thread Started -[0x00002d64] | DtManager | INFO | 2023-05-17 09:46:10,628 | Dt Worker Thread Started -[0x00007bbc] | DtManager | INFO | 2023-05-17 09:46:10,628 | Dt Data Handler Thread Started -[0x00002ad0] | DtManager | INFO | 2023-05-17 09:46:10,628 | Dt Worker Thread Started -[0x00004a48] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started -[0x00005084] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started -[0x00003050] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started -[0x00003e88] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started -[0x00006550] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started -[0x00007950] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started -[0x000059a8] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started -[0x00007de4] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started -[0x00007a24] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started -[0x00001eec] | DtManager | INFO | 2023-05-17 09:46:10,630 | Dt Worker Thread Started -[0x00001f5c] | DtManager | INFO | 2023-05-17 09:46:10,630 | Dt Worker Thread Started -[0x00007a50] | DtManager | INFO | 2023-05-17 09:46:10,630 | Dt Worker Thread Started -[0x000009c4] | DtManager | INFO | 2023-05-17 09:46:10,630 | Dt Worker Thread Started -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:10,633 | Show splash screen message: Initializing system components -[0x00001190] | GPU.MultiBoardMgr | INFO | 2023-05-17 09:46:10,635 | Let There Be CUDA Light! -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:10,639 | Show splash screen message: Loading video codecs -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:10,644 | Show splash screen message: Loading video plugins -[0x00004fe8] | UI.GLContext | INFO | 2023-05-17 09:46:10,680 | Creating shared OpenGL context for this thread (1 total). -[0x00004fe8] | UI.GLContext | INFO | 2023-05-17 09:46:10,708 | Initialized OpenGL 4.6 (requested 2.0) on device 'NVIDIA Corporation NVIDIA GeForce RTX 2060 SUPER/PCIe/SSE2' -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:10,708 | Show splash screen message: Loading Fairlight Engine -[0x00001190] | GPU.MultiBoardMgr | INFO | 2023-05-17 09:46:10,708 | Initializing CUDA board manager for Main Display GPU gpu:a24a3d50.83f1fdb7. -[0x00001968] | IO | INFO | 2023-05-17 09:46:10,788 | IO codec initialization completed in 143 ms. -[0x000065b4] | GPU.SingleBoardMgr | INFO | 2023-05-17 09:46:10,811 | Board manager thread for "NVIDIA GeForce RTX 2060 SUPER" (gpu:a24a3d50.83f1fdb7) is ready. -[0x00001190] | UI.GLInterop | INFO | 2023-05-17 09:46:10,811 | OpenGL interop was initialized. -[0x00001190] | UI.GLInterop | INFO | 2023-05-17 09:46:10,811 | OpenGL interop was initialized. -[0x00001190] | GPU.MultiBoardMgr | INFO | 2023-05-17 09:46:10,811 | Enabled CUDA pinned memory. -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.122(003): WASAPI: Scanning Speakers (Steam Streaming Microphone) for formats -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.128(000): WASAPI: Device: Speakers (Steam Streaming Microphone). Scan time 6. Formats = 1 15 formats scanned -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.128(000): WASAPI: Scanning Speakers (High Definition Audio Device) for formats -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.190(000): WASAPI: Device: Speakers (High Definition Audio Device). Scan time 61. Formats = 1 15 formats scanned -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.190(000): WASAPI: Scanning Mi 27 NU (NVIDIA High Definition Audio) for formats -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.236(000): WASAPI: Device: Mi 27 NU (NVIDIA High Definition Audio). Scan time 45. Formats = 1 15 formats scanned -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.236(000): WASAPI: Scanning Speakers (Steam Streaming Speakers) for formats -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.242(000): WASAPI: Device: Speakers (Steam Streaming Speakers). Scan time 6. Formats = 1 15 formats scanned -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.243(001): WASAPI: Scanning Digital Audio (S/PDIF) (High Definition Audio Device) for formats -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,212 | 00.00.00.286(000): WASAPI: Device: Digital Audio (S/PDIF) (High Definition Audio Device). Scan time 42. Formats = 1 15 formats scanned -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,212 | 00.00.00.287(001): WASAPI: Scanning Microphone (Steam Streaming Microphone) for formats -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,212 | 00.00.00.294(000): WASAPI: Device: Microphone (Steam Streaming Microphone). Scan time 7. Formats = 0 16 formats scanned -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,212 | 00.00.00.390(002): WASAPI: switching to AUTO because exclusive mode not allowed -[0x00004f94] | DbCommon2 | ERROR | 2023-05-17 09:46:11,307 | Cannot connect to soundfx192.168.100.31 database: could not connect to server: Connection refused (0x0000274D/10061) - Is the server running on host "192.168.100.31" and accepting - TCP/IP connections on port 5432? -QPSQL: Unable to connect -[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,339 | postgres project library homepc at 127.0.0.1 version 9.5.22 -[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,347 | Project library [homepc127.0.0.1] current version <18.1.0.004> updated on <2022-11-22T16:16:24.518>, remark: -[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,347 | Connect to postgres project library homepc127.0.0.1 -[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,396 | postgres project library barney at 192.168.100.30 version 9.5.25 -[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,422 | Project library [barney192.168.100.30] current version <18.1.0.004> updated on <2022-11-17T08:47:18.322>, remark: -[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,422 | Connect to postgres project library barney192.168.100.30 -[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,475 | postgres project library shtv_barney at 192.168.100.30 version 9.5.25 -[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,501 | Project library [shtv_barney192.168.100.30] current version <18.1.0.004> updated on <2022-11-15T13:16:50.006>, remark: -[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,501 | Connect to postgres project library shtv_barney192.168.100.30 -[0x00005850] | Fusion | INFO | 2023-05-17 09:46:11,704 | 260 templates scanned in 0.12 secs -[0x00004fe8] | Fairlight | INFO | 2023-05-17 09:46:12,310 | 00.00.01.565(000): Running Fairlight (939d5c56aa8d66c1f392ff963f9b9c349ef4d9fb) -[0x00004fe8] | FairlightLoader | INFO | 2023-05-17 09:46:12,310 | Fairlight lib initialized in 1598 ms. -[0x00006a7c] | UI.GLContext | INFO | 2023-05-17 09:46:12,405 | Creating shared OpenGL context for this thread (2 total). -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:12,417 | 00.00.01.501(002): WASAPI: switching to AUTO because exclusive mode not allowed -[0x00006a7c] | UI.GLIO | INFO | 2023-05-17 09:46:12,454 | Initialized MainPlayer OpenGL I/O on CUDA device 'NVIDIA GeForce RTX 2060 SUPER' -[0x00004fe8] | UI.GLIO | INFO | 2023-05-17 09:46:12,454 | MainPlayer: OpenGL I/O setup done. -[0x000076bc] | UI.GLContext | INFO | 2023-05-17 09:46:12,455 | Creating shared OpenGL context for this thread (3 total). -[0x000076bc] | UI.GLIO | INFO | 2023-05-17 09:46:12,505 | Initialized AuxPlayer OpenGL I/O on CUDA device 'NVIDIA GeForce RTX 2060 SUPER' -[0x00004fe8] | UI.GLIO | INFO | 2023-05-17 09:46:12,505 | AuxPlayer: OpenGL I/O setup done. -[0x00008190] | UI.GLContext | INFO | 2023-05-17 09:46:12,506 | Creating shared OpenGL context for this thread (4 total). -[0x00008190] | UI.Scopes | INFO | 2023-05-17 09:46:12,565 | Initialized GPU Scopes Manager on CUDA device 'NVIDIA GeForce RTX 2060 SUPER' -[0x00004fe8] | UI.MenuBar | WARN | 2023-05-17 09:46:12,649 | Main menu action [workspaceLayoutFusion_sub001Default]'s slot is not defined: workspaceLayoutFusion_sub001Default_triggered() -[0x00004fe8] | UI.MenuBar | WARN | 2023-05-17 09:46:12,649 | Main menu action [workspaceWIPlugins_placeholder]'s slot is not defined: workspaceWIPlugins_placeholder_triggered() -[0x00006e14] | SyManager | WARN | 2023-05-17 09:46:12,656 | socket failed to connect to server, error: 10061 - -[0x00006e14] | SyManager | ERROR | 2023-05-17 09:46:12,656 | DRIVER: panel connection failed -[0x00004fe8] | FusionUtils | WARN | 2023-05-17 09:46:12,657 | Fusion DoAction ACTION_SET_FUSION_HOTKEY ignored, init not completed -[0x000052e4] | BtCommon | INFO | 2023-05-17 09:46:12,661 | Starting Daemon: C:/Program Files/Blackmagic Design/DaVinci Resolve/DaVinciPanelDaemon.exe -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:12,686 | Show splash screen message: Loading Project Settings -[0x00006e14] | SyManager | INFO | 2023-05-17 09:46:12,757 | Connection to the panel server has been re-established -[0x00004fe8] | UI | WARN | 2023-05-17 09:46:12,899 | Failed to find value '0' in combo-box -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:19,055 | Show splash screen message: Loading Media Page -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:46:19,145 | Action [fairlightBusStructure] is not a global action -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:46:19,145 | Action [fairlightBusStructure] is not a valid global action -[0x00004fe8] | UI.FairlightInterface | WARN | 2023-05-17 09:46:19,269 | SetActionEnabled: Failed to find action [viewTimelineScrollingFixed]'s action connector for handler Id [1] -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:19,424 | Show splash screen message: Loading Cut Page -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:19,698 | Show splash screen message: Loading Edit Page -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:19,951 | Show splash screen message: Loading Fusion Page -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:20,038 | Show splash screen message: Loading Fairlight Page -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:20,227 | Show splash screen message: Loading Color Page -[0x00004fe8] | UI | INFO | 2023-05-17 09:46:20,296 | Not creating special GL widget for screen 0 -[0x00004fe8] | UI | WARN | 2023-05-17 09:46:21,003 | Failed to find value '8192' in combo-box -[0x00004fe8] | UI | WARN | 2023-05-17 09:46:21,284 | Failed to find value '100' in combo-box -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:22,006 | Show splash screen message: Loading Waveform Monitor -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:22,433 | Show splash screen message: Loading Audio Plugins -[0x00004fe8] | SyManager | INFO | 2023-05-17 09:46:22,528 | Collaboration IP 127.0.0.1 Port 0 -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:22,674 | Show splash screen message: Loading Projects -[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:46:22,739 | Current user pointer is changed -[0x00004fe8] | UI | WARN | 2023-05-17 09:46:22,755 | UI Persistence - SecondaryScreenIdx not read -[0x00004fe8] | UI | WARN | 2023-05-17 09:46:22,755 | UI Persistence - UseDisplayNameForClips not read -[0x00004fe8] | UI | WARN | 2023-05-17 09:46:22,755 | UI Persistence - DefaultTransitionKey not read -[0x00004fe8] | UI | WARN | 2023-05-17 09:46:22,755 | UI Persistence - DefaultAudioTransitionKey not read -[0x00004fe8] | IO | INFO | 2023-05-17 09:46:22,767 | RED rocket decode has been disabled in the config file -[0x00004fe8] | LeManager | ERROR | 2023-05-17 09:46:22,820 | 444, 139, 0 -[0x00004fe8] | SyManager.DeckLink | ERROR | 2023-05-17 09:46:22,922 | Failed to create instance. -[0x00004fe8] | SyManager | INFO | 2023-05-17 09:46:22,973 | Search Parameter (-2) or Search Operation (0) not supported, and hence disabling the condition. -[0x00004fe8] | SyManager | INFO | 2023-05-17 09:46:22,973 | Search Parameter (-1) or Search Operation (-1) not supported, and hence disabling the condition. -[0x00004fe8] | UI | WARN | 2023-05-17 09:46:23,077 | Failed to find value '0' in combo-box -[0x00004fe8] | UI | WARN | 2023-05-17 09:46:23,080 | Failed to find value '0' in combo-box -[0x00004fe8] | UI | WARN | 2023-05-17 09:46:23,098 | Failed to find value '0' in combo-box -[0x00004fe8] | UI | WARN | 2023-05-17 09:46:23,099 | Failed to find value '4' in combo-box -[0x00004fe8] | SyManager.Gallery | INFO | 2023-05-17 09:46:23,119 | Start purging still caches -[0x00004fe8] | SyManager.Gallery | INFO | 2023-05-17 09:46:23,119 | Finish purging still caches -[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:46:23,149 | Media pool relink status changed to 0 -[0x00004fe8] | FusionUtils | WARN | 2023-05-17 09:46:23,167 | Fusion DoAction ACTION_SET_FUSION_HOTKEY ignored, init not completed -[0x00004fe8] | UI | INFO | 2023-05-17 09:46:23,179 | Launching project manager -[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:46:23,182 | Main view page is changed to 12 -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:23,209 | Show splash screen message: Ready -[0x00004fe8] | UI | INFO | 2023-05-17 09:46:23,214 | Gallery pointer is changed, refreshing color gallery -[0x00004fe8] | UI | INFO | 2023-05-17 09:46:23,223 | Gallery pointer is changed, refreshing gallery browser -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:23,312 | Close splash screen -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:23,314 | Launching main loop -[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:46:24,215 | Fusion templates changed -[0x00004fe8] | Fusion | INFO | 2023-05-17 09:46:24,758 | Started script server: 32648 - -[0x00004fe8] | SyManager.ActionExecutor | INFO | 2023-05-17 09:48:41,800 | Database transaction is ongoing, user initiated action Sync Asset Map is postponed -[0x00004fe8] | SyManager.ActionExecutor | INFO | 2023-05-17 09:48:41,800 | Database transaction completed, enqueueing 1 postponed actions -[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:48:41,842 | Loading project (gazprom_screens_06_R_Home_Painter_Lookdev_v002) from project library (homepc127.0.0.1) took 291 ms -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:42,159 | Action [fairlightBusStructure] is not a global action -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:48:42,390 | 00.02.31.644(002): WASAPI: switching to AUTO because exclusive mode not allowed -[0x00004fe8] | UI | WARN | 2023-05-17 09:48:43,528 | Failed to find value '0' in combo-box -[0x00004fe8] | UI | WARN | 2023-05-17 09:48:43,531 | Failed to find value '0' in combo-box -[0x00004fe8] | SyManager.Gallery | INFO | 2023-05-17 09:48:43,539 | Start purging still caches -[0x00004fe8] | SyManager.Gallery | INFO | 2023-05-17 09:48:43,539 | Finish purging still caches -[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:48:43,557 | Media pool relink status changed to 0 -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:48:43,596 | 00.02.32.735(002): WASAPI: switching to AUTO because exclusive mode not allowed -[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:48:43,862 | Current project pointer (gazprom_screens_06_R_Home_Painter_Lookdev_v002) is changed -[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:48:43,862 | Current timeline pointer () is changed -[0x00004fe8] | UI | INFO | 2023-05-17 09:48:43,868 | Lock project 513e336f-59c3-4fa8-b9e9-814cf2b797a3 -[0x00004fe8] | Undefined | INFO | 2023-05-17 09:48:43,868 | The buttonId [7] is not found for [0] in [0] -[0x00004fe8] | UI | INFO | 2023-05-17 09:48:43,876 | UiGPU::UploadWipeData called before UiGPU has been initialized. Returning false -[0x00004fe8] | UI | INFO | 2023-05-17 09:48:43,877 | Gallery pointer is changed, refreshing color gallery -[0x00004fe8] | UI | INFO | 2023-05-17 09:48:43,891 | Gallery pointer is changed, refreshing gallery browser -[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:48:43,921 | Main view page is changed to 1 -[0x00004fe8] | UI | WARN | 2023-05-17 09:48:43,921 | Failed to get auto update information. -[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:48:43,930 | Main view page is changed to 1 -[0x00004fe8] | UI | WARN | 2023-05-17 09:48:44,566 | UI Persistence - MediaPoolFloatingWindowGeometry not read -[0x00004fe8] | Undefined | INFO | 2023-05-17 09:48:44,583 | The buttonId [7] is not found for [0] in [0] -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:48:44,602 | 00.02.33.795(001): WASAPI: switching to AUTO because exclusive mode not allowed -[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:48:44,912 | Main view page is changed to 1 -[0x00004fe8] | UI | INFO | 2023-05-17 09:48:45,076 | Launching main window -[0x00004fe8] | SyManager | WARN | 2023-05-17 09:48:45,094 | No reply received from file system, assume successfully deleted folder D:\SYNC\BACKUP\Resolve Project Backups\64d8dbe8-256e-46b1-81da-671a6a8fc423. -[0x00004fe8] | SyManager | WARN | 2023-05-17 09:48:45,105 | No reply received from file system, assume successfully deleted folder C:\Users\videopro\Videos\CacheClip\64d8dbe8-256e-46b1-81da-671a6a8fc423. -[0x00004fe8] | SyManager | WARN | 2023-05-17 09:48:45,116 | No reply received from file system, assume successfully deleted folder C:\Users\videopro\Videos\.gallery\64d8dbe8-256e-46b1-81da-671a6a8fc423. -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,122 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_newProject] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,122 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_newFolder] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,122 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_openProject] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,122 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_openProjectInReadOnlyMode] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,122 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_Close] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,122 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_Rename] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,122 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_saveProject] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,122 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_saveProjectAs] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_importProject] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_importProjectPlus] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_exportProject] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_exportProjectWithStillsAndLuts] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_restoreProject] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_restoreProjectPlus] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_archiveProject] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_unlockProject] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [editDelete] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [editCut] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [editCopy] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [editPaste] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_loadProjectSettingsToCurrentProject] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_projectBackups] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_projectSettings] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_dynamicProjectSwitching] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_closeProjectsInMemory] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_updateThumbnails] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_otherProjectBackups] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_refresh] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_resetUiLayout] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:51,277 | Action owner [MediaPool] is not active when refreshing shortcut [editDuplicateTimelineOrClips] due to [No focused widget is set yet] -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:01,060 | Traceback (most recent call last): -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:01,060 | File "", line 1, in -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:01,061 | AttributeError -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:01,061 | : -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:01,061 | module 'sys' has no attribute '__path__' -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:01,061 | -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:08,715 | Traceback (most recent call last): -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:08,715 | File "", line 1, in -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:08,715 | AttributeError -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:08,716 | : -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:08,716 | module 're' has no attribute '__path__' -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:08,716 | -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:21,531 | Traceback (most recent call last): -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:21,531 | File "", line 1, in -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:21,532 | TypeError -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:21,532 | : -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:21,532 | split() missing 2 required positional arguments: 'pattern' and 'string' -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:21,532 | -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:29,443 | Traceback (most recent call last): -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:29,443 | File "", line 1, in -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:29,444 | NameError -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:29,444 | : -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:29,444 | name 're__path__' is not defined -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:29,444 | -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:33,563 | Traceback (most recent call last): -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:33,563 | File "", line 1, in -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:33,564 | AttributeError -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:33,564 | : -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:33,564 | module 're' has no attribute '__path__' -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:33,564 | -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:39,740 | Traceback (most recent call last): -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:39,740 | File "", line 1, in -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:39,740 | AttributeError -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:39,740 | : -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:39,740 | module 'sys' has no attribute 'info' -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:39,740 | -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:41,372 | Traceback (most recent call last): -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:41,372 | File "", line 1, in -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:41,372 | AttributeError -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:41,372 | : -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:41,372 | module 'sys' has no attribute 'info' -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:41,372 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:50:52,078 | >>> [ openpype.hosts.resolve installed ] -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:50:52,083 | >>> [ Registering DaVinci Resovle plug-ins.. ] -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:50:52,100 | >>> [ Assigning resolve module to `pype.hosts.resolve.api.bmdvr`: Resolve (0x00007FF6BB5340D0) [App: 'Resolve' on 127.0.0.1, UUID: f0d29a26-23ed-495a-9eb5-f264f7187252] ] -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:50:52,105 | >>> [ Assigning resolve module to `pype.hosts.resolve.api.bmdvf`: FusionUI (0x0000023A629E7040) [App: 'Resolve' on 127.0.0.1, UUID: f0d29a26-23ed-495a-9eb5-f264f7187252] ] -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:50:52,110 | - { timers_manager }: [ Installing task changed callback ] -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:51:12,816 | - { openpype.pipeline.anatomy }: [ Looking for matching root in path "D:/SYNC/OPENPYPE/gazprom_screens/06_R_Home_Painter/work/Lookdev". ] -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:51:12,821 | >>> [ Found match in root "work". ] -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,952 | Traceback (most recent call last): -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,953 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\subsetmanager\window.py", line 190, in showEvent -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,953 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,953 | self.refresh() -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,953 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,953 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\subsetmanager\window.py", line 176, in refresh -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,953 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,954 | self._model.refresh() -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,954 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,954 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\subsetmanager\model.py", line 27, in refresh -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,954 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,954 | instances = list_instances() -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,954 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,954 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\hosts\resolve\api\pipeline.py", line 285, in list_instances -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,955 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,955 | selected_timeline_items = lib.get_current_timeline_items( -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,955 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,955 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\hosts\resolve\api\lib.py", line 319, in get_current_timeline_items -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,955 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,955 | selected_track_count = timeline.GetTrackCount(track_type) -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,955 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,956 | AttributeError -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,956 | : -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,956 | 'NoneType' object has no attribute 'GetTrackCount' -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,956 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,000 | Traceback (most recent call last): -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,000 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\libraryloader\app.py", line 376, in _assetschanged -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,000 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,000 | subsets_model.set_assets(asset_ids) -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,001 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,001 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\loader\model.py", line 259, in set_assets -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,001 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,001 | self.refresh() -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,001 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,001 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\loader\model.py", line 577, in refresh -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,001 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,002 | repre_ids = {con.get("representation") for con in containers} -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,002 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,002 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\loader\model.py", line 577, in -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,002 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,002 | repre_ids = {con.get("representation") for con in containers} -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,002 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,002 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\hosts\resolve\api\pipeline.py", line 138, in ls -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,003 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,003 | all_timeline_items = lib.get_current_timeline_items(filter=False) -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,003 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,003 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\hosts\resolve\api\lib.py", line 319, in get_current_timeline_items -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,003 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,003 | selected_track_count = timeline.GetTrackCount(track_type) -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,003 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,003 | AttributeError -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,004 | : -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,004 | 'NoneType' object has no attribute 'GetTrackCount' -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,004 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,731 | Traceback (most recent call last): -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,732 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\libraryloader\app.py", line 376, in _assetschanged -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,732 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,732 | subsets_model.set_assets(asset_ids) -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,732 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,732 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\loader\model.py", line 259, in set_assets -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,732 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,733 | self.refresh() -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,733 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,733 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\loader\model.py", line 577, in refresh -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,733 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,733 | repre_ids = {con.get("representation") for con in containers} -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,733 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,734 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\loader\model.py", line 577, in -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,734 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,734 | repre_ids = {con.get("representation") for con in containers} -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,734 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,734 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\hosts\resolve\api\pipeline.py", line 138, in ls -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,734 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,734 | all_timeline_items = lib.get_current_timeline_items(filter=False) -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,735 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,735 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\hosts\resolve\api\lib.py", line 319, in get_current_timeline_items -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,735 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,735 | selected_track_count = timeline.GetTrackCount(track_type) -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,735 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,735 | AttributeError -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,735 | : -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,735 | 'NoneType' object has no attribute 'GetTrackCount' -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,736 | -[0x00004fe8] | UI | WARN | 2023-05-17 09:53:56,329 | UI Persistence - MediaPoolFloatingWindowGeometry not read -[0x00004fe8] | UI | WARN | 2023-05-17 09:53:56,329 | UI Persistence - ConformEdlEffectsLibrary not read -[0x000065b4] | GPU.SingleBoardMgr | INFO | 2023-05-17 09:53:56,766 | Flushing GPU memory... -[0x000065b4] | UI.GLContext | INFO | 2023-05-17 09:53:56,766 | Creating shared OpenGL context for this thread (5 total). -[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:53:56,782 | Main view page is changed to 2 -[0x000065b4] | UI.GLTexPool | INFO | 2023-05-17 09:53:56,884 | Released 0 MiB in 0 unused textures. -[0x00004fe8] | UI | INFO | 2023-05-17 09:53:56,944 | PBO is initialized with size [1920x1080], bitDepth=[8], hasAlpha=[1]. -[0x00004fe8] | UI | WARN | 2023-05-17 09:53:59,491 | Failed to find value '1' in combo-box -[0x00004fe8] | UI | WARN | 2023-05-17 09:53:59,491 | Failed to find value '2' in combo-box -[0x00004fe8] | UI | WARN | 2023-05-17 09:53:59,491 | Failed to find value '2' in combo-box -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:53:59,548 | Action owner [MediaPool] is not active when refreshing shortcut [editDuplicateTimelineOrClips] due to [No focused widget is set yet] -[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:54:01,297 | Current timeline pointer (Timeline 1) is changed -[0x00004fe8] | UI | ERROR | 2023-05-17 09:54:01,405 | No marker found at frame -90000 to delete. -[0x00004fe8] | UI | ERROR | 2023-05-17 09:54:01,405 | No marker found at frame -90000 to delete. -[0x00004fe8] | UI | ERROR | 2023-05-17 09:54:01,405 | No marker found at frame -90000 to delete. -[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:01,495 | Start saving project gazprom_screens_06_R_Home_Painter_Lookdev_v002 -[0x000083dc] | SyManager.ActionExecutor | INFO | 2023-05-17 09:54:01,526 | Database transaction is ongoing, user initiated action SmActIOCPSigInvoker is postponed -[0x000083dc] | SyManager.ActionExecutor | INFO | 2023-05-17 09:54:01,550 | Database transaction is ongoing, user initiated action SmActIOCPSigInvoker is postponed -[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:01,592 | Saving project took 97 ms -[0x00004fe8] | SyManager.ActionExecutor | INFO | 2023-05-17 09:54:01,619 | Database transaction completed, enqueueing 2 postponed actions -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:54:23,357 | >>> [ openpype.hosts.resolve installed ] -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:54:23,363 | >>> [ Registering DaVinci Resovle plug-ins.. ] -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:54:23,375 | >>> [ Assigning resolve module to `pype.hosts.resolve.api.bmdvr`: Resolve (0x00007FF6BB5340D0) [App: 'Resolve' on 127.0.0.1, UUID: f0d29a26-23ed-495a-9eb5-f264f7187252] ] -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:54:23,380 | >>> [ Assigning resolve module to `pype.hosts.resolve.api.bmdvf`: FusionUI (0x0000023A629E7040) [App: 'Resolve' on 127.0.0.1, UUID: f0d29a26-23ed-495a-9eb5-f264f7187252] ] -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:54:23,386 | - { timers_manager }: [ Installing task changed callback ] -[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:39,823 | Start saving project gazprom_screens_06_R_Home_Painter_Lookdev_v002 -[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:39,826 | Saving project took 2 ms -[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:39,920 | Start saving project gazprom_screens_06_R_Home_Painter_Lookdev_v002 -[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:39,923 | Saving project took 3 ms -[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:40,137 | Start saving project gazprom_screens_06_R_Home_Painter_Lookdev_v002 -[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:40,321 | Saving project took 183 ms -[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:54:40,326 | Main view page is changed to 2 -[0x00004fe8] | BtCommon | WARN | 2023-05-17 09:54:40,570 | Negative duration -[0x00004fe8] | UI | WARN | 2023-05-17 09:54:40,638 | Unable to submit frame to GPU scopes, legacy OpenGL uploads not supported (Player Model). -[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:42,497 | Start saving project gazprom_screens_06_R_Home_Painter_Lookdev_v002 -[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:42,555 | Saving project took 59 ms From 8b6feb38ab7a6d71b82b27dce0c9a807cff291ed Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov Date: Wed, 17 May 2023 10:55:41 +0300 Subject: [PATCH 061/189] Bring the PATH setting back --- openpype/hosts/resolve/hooks/pre_resolve_setup.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/openpype/hosts/resolve/hooks/pre_resolve_setup.py b/openpype/hosts/resolve/hooks/pre_resolve_setup.py index 486d8121cf..549777c34e 100644 --- a/openpype/hosts/resolve/hooks/pre_resolve_setup.py +++ b/openpype/hosts/resolve/hooks/pre_resolve_setup.py @@ -84,6 +84,15 @@ class ResolvePrelaunch(PreLaunchHook): self.log.debug(f"PYTHONPATH: {self.launch_context.env['PYTHONPATH']}") + # add to the python path to PATH + env_path = self.launch_context.env["PATH"] + self.log.info(f"Adding `{python3_home_str}` to the PATH variable") + self.launch_context.env[ + "PATH" + ] = f"{python3_home_str}{os.pathsep}{env_path}" + + self.log.debug(f"PATH: {self.launch_context.env['PATH']}") + resolve_utility_scripts_dirs = { "windows": ( f"{programdata}/Blackmagic Design" From 05a25bb30b8770aad253fe9ecbe3609205c09923 Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov Date: Wed, 17 May 2023 11:46:06 +0300 Subject: [PATCH 062/189] add docstrings --- .../hosts/resolve/hooks/pre_resolve_setup.py | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/resolve/hooks/pre_resolve_setup.py b/openpype/hosts/resolve/hooks/pre_resolve_setup.py index 549777c34e..2cdeb8c4ff 100644 --- a/openpype/hosts/resolve/hooks/pre_resolve_setup.py +++ b/openpype/hosts/resolve/hooks/pre_resolve_setup.py @@ -7,10 +7,27 @@ from openpype.hosts.resolve.utils import setup class ResolvePrelaunch(PreLaunchHook): """ - This hook will check if current workfile path has Resolve - project inside. IF not, it initialize it and finally it pass - path to the project by environment variable to Premiere launcher - shell script. + This hook will set up the Resolve scripting environment as described in + Resolve's documentation found with the installed application at + {resolve}/Support/Developer/Scripting/README.txt + + Prepares the following environment variables: + - `RESOLVE_SCRIPT_API` + - `RESOLVE_SCRIPT_LIB` + + It adds $RESOLVE_SCRIPT_API/Modules to PYTHONPATH. + + Additionally it sets up the Python home for Python 3 based on the + RESOLVE_PYTHON3_HOME in the environment (usually defined in OpenPype's + Application environment for Resolve by the admin). For this it sets + PYTHONHOME and PATH variables. + + It also defines: + - `RESOLVE_UTILITY_SCRIPTS_DIR`: Destination directory for OpenPype + Fusion scripts to be copied to for Resolve to pick them up. + - `OPENPYPE_LOG_NO_COLORS` to True to ensure OP doesn't try to + use logging with terminal colors as it fails in Resolve. + """ app_groups = ["resolve"] From a5fde9663805326f1fea5de5b278e4f429d85e1a Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov Date: Wed, 17 May 2023 11:47:08 +0300 Subject: [PATCH 063/189] formatting --- openpype/hosts/resolve/hooks/pre_resolve_setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/resolve/hooks/pre_resolve_setup.py b/openpype/hosts/resolve/hooks/pre_resolve_setup.py index 2cdeb8c4ff..6747e773a3 100644 --- a/openpype/hosts/resolve/hooks/pre_resolve_setup.py +++ b/openpype/hosts/resolve/hooks/pre_resolve_setup.py @@ -26,7 +26,7 @@ class ResolvePrelaunch(PreLaunchHook): - `RESOLVE_UTILITY_SCRIPTS_DIR`: Destination directory for OpenPype Fusion scripts to be copied to for Resolve to pick them up. - `OPENPYPE_LOG_NO_COLORS` to True to ensure OP doesn't try to - use logging with terminal colors as it fails in Resolve. + use logging with terminal colors as it fails in Resolve. """ From da29890d9be2634d92115c6e960ba066e4e48954 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Wed, 17 May 2023 13:48:34 +0200 Subject: [PATCH 064/189] Update openpype/hosts/fusion/plugins/publish/extract_render_local.py Co-authored-by: Roy Nieterau --- openpype/hosts/fusion/plugins/publish/extract_render_local.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/publish/extract_render_local.py b/openpype/hosts/fusion/plugins/publish/extract_render_local.py index f093f7793f..f801f30577 100644 --- a/openpype/hosts/fusion/plugins/publish/extract_render_local.py +++ b/openpype/hosts/fusion/plugins/publish/extract_render_local.py @@ -83,7 +83,7 @@ class FusionRenderLocal( # Only active instances instance.data.get("publish", True) and # Only render.local instances - "render.local" in instance.data.get("families") + "render.local" in instance.data.get("families", []) ] if key not in context.data: From 0f80ad01ec243e08e6d7824772861a2b655c64f6 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 17 May 2023 14:16:49 +0200 Subject: [PATCH 065/189] adding deadline settings including Pools --- .../plugins/publish/submit_fusion_deadline.py | 9 ++-- .../defaults/project_settings/deadline.json | 9 ++++ .../schema_project_deadline.json | 44 +++++++++++++++++++ 3 files changed, 56 insertions(+), 6 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py index d51299506c..717391100d 100644 --- a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py @@ -38,10 +38,6 @@ class FusionSubmitDeadline( chunk_size = 1 concurrent_tasks = 1 group = "" - department = "" - limit_groups = {} - env_allowed_keys = [] - env_search_replace_values = {} @classmethod def get_attribute_defs(cls): @@ -173,8 +169,9 @@ class FusionSubmitDeadline( # User, as seen in Monitor "UserName": deadline_user, - # Use a default submission pool for Fusion - "Pool": "fusion", + "Pool": instance.data.get("primaryPool"), + "SecondaryPool": instance.data.get("secondaryPool"), + "Group": self.group, "Plugin": "Fusion", "Frames": "{start}-{end}".format( diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json index 3f114025f3..1b8c8397d7 100644 --- a/openpype/settings/defaults/project_settings/deadline.json +++ b/openpype/settings/defaults/project_settings/deadline.json @@ -45,6 +45,15 @@ "chunk_size": 10, "group": "none" }, + "FusionSubmitDeadline": { + "enabled": true, + "optional": false, + "active": true, + "priority": 50, + "chunk_size": 10, + "concurrent_tasks": 1, + "group": "" + }, "NukeSubmitDeadline": { "enabled": true, "optional": false, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json index d8b5e4dc1f..6d59b5a92b 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json @@ -248,6 +248,50 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "key": "FusionSubmitDeadline", + "label": "Fusion submit to Deadline", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "optional", + "label": "Optional" + }, + { + "type": "boolean", + "key": "active", + "label": "Active" + }, + { + "type": "number", + "key": "priority", + "label": "Priority" + }, + { + "type": "number", + "key": "chunk_size", + "label": "Frame per Task" + }, + { + "type": "number", + "key": "concurrent_tasks", + "label": "Number of concurrent tasks" + }, + { + "type": "text", + "key": "group", + "label": "Group Name" + } + ] + }, { "type": "dict", "collapsible": true, From a6059afe869aed7996d44699f87ef30e338da4ae Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 17 May 2023 14:37:37 +0200 Subject: [PATCH 066/189] pr comments also renamed start_handle as it is easily confusable with handles --- .../fusion/plugins/create/create_saver.py | 2 +- .../plugins/publish/collect_instances.py | 41 +++++++++++-------- .../fusion/plugins/publish/collect_render.py | 2 +- 3 files changed, 25 insertions(+), 20 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index d5e77730c8..28917cb27d 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -233,7 +233,7 @@ class CreateSaver(NewCreator): def _get_frame_range_enum(self): frame_range_options = { "asset_db": "From asset database", - "viewer_render_range": "From viewer render in/out", + "render_range": "From viewer render in/out", "comp_range": "From composition timeline" } diff --git a/openpype/hosts/fusion/plugins/publish/collect_instances.py b/openpype/hosts/fusion/plugins/publish/collect_instances.py index 59ff52f5b2..98ea0a34e4 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_instances.py +++ b/openpype/hosts/fusion/plugins/publish/collect_instances.py @@ -20,25 +20,28 @@ class CollectInstanceData(pyblish.api.InstancePlugin): # Include creator attributes directly as instance data creator_attributes = instance.data["creator_attributes"] - frame_range_source = creator_attributes.get("frame_range_source") instance.data.update(creator_attributes) - # get asset frame ranges - start = context.data["frameStart"] - end = context.data["frameEnd"] - handle_start = context.data["handleStart"] - handle_end = context.data["handleEnd"] - start_handle = start - handle_start - end_handle = end + handle_end + frame_range_source = creator_attributes.get("frame_range_source") + instance.data["frame_range_source"] = frame_range_source - if frame_range_source == "viewer_render_range": + if frame_range_source == "asset_db": + # get asset frame ranges + start = context.data["frameStart"] + end = context.data["frameEnd"] + handle_start = context.data["handleStart"] + handle_end = context.data["handleEnd"] + start_with_handle = start - handle_start + end_with_handle = end + handle_end + + if frame_range_source == "render_range": # set comp render frame ranges start = context.data["renderFrameStart"] end = context.data["renderFrameEnd"] handle_start = 0 handle_end = 0 - start_handle = start - end_handle = end + start_with_handle = start + end_with_handle = end if frame_range_source == "comp_range": comp_start = context.data["compFrameStart"] @@ -50,14 +53,16 @@ class CollectInstanceData(pyblish.api.InstancePlugin): end = render_end handle_start = render_start - comp_start handle_end = comp_end - render_end - start_handle = comp_start - end_handle = comp_end + start_with_handle = comp_start + end_with_handle = comp_end # Include start and end render frame in label subset = instance.data["subset"] - label = "{subset} ({start}-{end})".format(subset=subset, - start=int(start), - end=int(end)) + label = "{subset} ({start}-{end})".format( + subset=subset, + start=int(start), + end=int(end) + ) instance.data.update({ "label": label, @@ -65,8 +70,8 @@ class CollectInstanceData(pyblish.api.InstancePlugin): # todo: Allow custom frame range per instance "frameStart": start, "frameEnd": end, - "frameStartHandle": start_handle, - "frameEndHandle": end_handle, + "frameStartHandle": start_with_handle, + "frameEndHandle": end_with_handle, "handleStart": handle_start, "handleEnd": handle_end, "fps": context.data["fps"], diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py index 6956b566ad..dd6d9c2567 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_render.py +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -84,7 +84,7 @@ class CollectFusionRender( handleStart=inst.data["handleStart"], handleEnd=inst.data["handleEnd"], ignoreFrameHandleCheck=( - not inst.data.get("viewer_render_range")), + inst.data["frame_range_source"] == "render_range"), frameStep=1, fps=comp_frame_format_prefs.get("Rate"), app_version=comp.GetApp().Version, From 497a97b70d1eef294e0b7d2ea14365f5add380b4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 17 May 2023 14:40:47 +0200 Subject: [PATCH 067/189] pr comment --- openpype/hosts/fusion/plugins/create/create_saver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 28917cb27d..f1e7791972 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -232,7 +232,7 @@ class CreateSaver(NewCreator): def _get_frame_range_enum(self): frame_range_options = { - "asset_db": "From asset database", + "asset_db": "Current asset context", "render_range": "From viewer render in/out", "comp_range": "From composition timeline" } From fb3c4b613ffd08ac5677a11c2a42f9d217770ca7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 17 May 2023 14:47:32 +0200 Subject: [PATCH 068/189] improving label --- .../hosts/fusion/plugins/publish/collect_instances.py | 8 ++++++-- openpype/hosts/fusion/plugins/publish/collect_render.py | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_instances.py b/openpype/hosts/fusion/plugins/publish/collect_instances.py index 98ea0a34e4..458f00c7ed 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_instances.py +++ b/openpype/hosts/fusion/plugins/publish/collect_instances.py @@ -58,10 +58,14 @@ class CollectInstanceData(pyblish.api.InstancePlugin): # Include start and end render frame in label subset = instance.data["subset"] - label = "{subset} ({start}-{end})".format( + label = ( + "{subset} ({start}-{end}) [{handle_start}-{handle_end}]" + ).format( subset=subset, start=int(start), - end=int(end) + end=int(end), + handle_start=int(handle_start), + handle_end=int(handle_end) ) instance.data.update({ diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py index dd6d9c2567..c13d2a0c99 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_render.py +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -64,7 +64,7 @@ class CollectFusionRender( version=version, time="", source=current_file, - label="{} - {}".format(subset_name, family), + label=inst.data["label"], subset=subset_name, asset=inst.data["asset"], task=task_name, From 089fe88ee104c7f0c3d7d85f66d9d03f3aafbbbf Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 17 May 2023 14:53:09 +0200 Subject: [PATCH 069/189] task is on context --- openpype/hosts/fusion/plugins/publish/collect_render.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py index c13d2a0c99..0a850a4982 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_render.py +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -51,7 +51,7 @@ class CollectFusionRender( if family != "render": continue - task_name = inst.data.get("task") # legacy + task_name = context.data["task"] tool = inst.data["transientData"]["tool"] instance_families = inst.data.get("families", []) From 163756d74c93ad74b5edf0095ee2279de66fabef Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 17 May 2023 14:58:24 +0200 Subject: [PATCH 070/189] adding comment for ambiguous function call --- openpype/hosts/fusion/plugins/publish/collect_render.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py index 0a850a4982..cbb9fea76d 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_render.py +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -125,6 +125,7 @@ class CollectFusionRender( def post_collecting_action(self): for instance in self._context: if "render.frames" in instance.data.get("families", []): + # adding representation data to the instance self._update_for_frames(instance) def get_expected_files(self, render_instance): From 40426cd69c4c70723254d0b301d7060fc858a1f7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 17 May 2023 15:03:21 +0200 Subject: [PATCH 071/189] simplifying code --- .../fusion/plugins/publish/collect_render.py | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py index cbb9fea76d..d0b7f1c4ff 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_render.py +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -32,8 +32,8 @@ class CollectFusionRender( comp = context.data.get("currentComp") comp_frame_format_prefs = comp.GetPrefs("Comp.FrameFormat") - aspect_x = comp_frame_format_prefs.get("AspectX") - aspect_y = comp_frame_format_prefs.get("AspectY") + aspect_x = comp_frame_format_prefs["AspectX"] + aspect_y = comp_frame_format_prefs["AspectY"] instances = [] instances_to_remove = [] @@ -93,14 +93,14 @@ class CollectFusionRender( render_target = inst.data["creator_attributes"]["render_target"] - if render_target == "local": - # for local renders - self._instance_data_local_update( - project_entity, instance, f"render.{render_target}") + # Add render target family + render_target_family = f"render.{render_target}" + if render_target_family not in instance.families: + instance.families.append(render_target_family) - if render_target == "frames": - self._instance_data_local_update( - project_entity, instance, f"render.{render_target}") + # Add render target specific data + if render_target in {"local", "frames"}: + instance.projectEntity = project_entity if render_target == "farm": fam = "render.farm" @@ -205,8 +205,3 @@ class CollectFusionRender( instance.data["representations"].append(repre) return instance - - def _instance_data_local_update(self, project_entity, instance, family): - instance.projectEntity = project_entity - if family not in instance.families: - instance.families.append(family) From 25832ed496f79063c4c2f0c3f9c87eb0e4b227c9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 17 May 2023 15:07:10 +0200 Subject: [PATCH 072/189] collect frame range simplification --- .../publish/collect_comp_frame_range.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py b/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py index 08bdad3120..24a9a92337 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py +++ b/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py @@ -34,16 +34,11 @@ class CollectFusionCompFrameRanges(pyblish.api.ContextPlugin): comp = context.data["currentComp"] # Store comp render ranges - ( - start, end, - global_start, - global_end, - ) = get_comp_render_range(comp) + start, end, global_start, global_end = get_comp_render_range(comp) - data = {} - data["renderFrameStart"] = int(start) - data["renderFrameEnd"] = int(end) - data["compFrameStart"] = int(global_start) - data["compFrameEnd"] = int(global_end) - - context.data.update(data) + context.data.update({ + "renderFrameStart": int(start), + "renderFrameEnd": int(end), + "compFrameStart": int(global_start), + "compFrameEnd": int(global_end) + }) From 6d9c0e30802db386476f5541732526abfa77265e Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 17 May 2023 15:46:34 +0100 Subject: [PATCH 073/189] Added setting for base file unit scale --- openpype/settings/defaults/project_settings/blender.json | 1 + .../schemas/projects_schema/schema_project_blender.json | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/openpype/settings/defaults/project_settings/blender.json b/openpype/settings/defaults/project_settings/blender.json index 20eec0c09d..0b3f38a40f 100644 --- a/openpype/settings/defaults/project_settings/blender.json +++ b/openpype/settings/defaults/project_settings/blender.json @@ -1,4 +1,5 @@ { + "base_file_unit_scale": 0.01, "imageio": { "ocio_config": { "enabled": false, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json index 725d9bfb08..00414b3210 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json @@ -5,6 +5,12 @@ "label": "Blender", "is_file": true, "children": [ + { + "key": "base_file_unit_scale", + "type": "number", + "label": "Base File Unit Scale", + "decimal": 2 + }, { "key": "imageio", "type": "dict", From 3350e0995f43094a93a2e2a54abba7b52a7916fe Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 17 May 2023 15:47:38 +0100 Subject: [PATCH 074/189] Set base unit scale when opening a file or creating a new one --- openpype/hosts/blender/api/pipeline.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/openpype/hosts/blender/api/pipeline.py b/openpype/hosts/blender/api/pipeline.py index c2aee1e653..02b1560e56 100644 --- a/openpype/hosts/blender/api/pipeline.py +++ b/openpype/hosts/blender/api/pipeline.py @@ -26,6 +26,8 @@ from openpype.lib import ( emit_event ) import openpype.hosts.blender +from openpype.settings import get_project_settings + HOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.blender.__file__)) PLUGINS_DIR = os.path.join(HOST_DIR, "plugins") @@ -122,12 +124,23 @@ def set_start_end_frames(): scene.render.resolution_y = resolution_y +def set_base_file_unit_scale(): + project = os.environ.get("AVALON_PROJECT") + settings = get_project_settings(project) + + unit_scale = settings.get("blender").get("base_file_unit_scale") + + bpy.context.scene.unit_settings.scale_length = unit_scale + + def on_new(): set_start_end_frames() + set_base_file_unit_scale() def on_open(): set_start_end_frames() + set_base_file_unit_scale() @bpy.app.handlers.persistent From 9ea2bb0c4d039636fd27142c12a7c0e87119d6cd Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 18 May 2023 15:24:19 +0800 Subject: [PATCH 075/189] use pymxs instead of maxscript for fbx loader --- .../hosts/max/plugins/load/load_camera_fbx.py | 24 +++++++------------ .../hosts/max/plugins/load/load_model_fbx.py | 22 +++++++---------- 2 files changed, 17 insertions(+), 29 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_camera_fbx.py b/openpype/hosts/max/plugins/load/load_camera_fbx.py index ce4dec32a0..9767321e3d 100644 --- a/openpype/hosts/max/plugins/load/load_camera_fbx.py +++ b/openpype/hosts/max/plugins/load/load_camera_fbx.py @@ -20,21 +20,15 @@ class FbxLoader(load.LoaderPlugin): from pymxs import runtime as rt filepath = os.path.normpath(self.fname) - - fbx_import_cmd = ( - f""" - -FBXImporterSetParam "Animation" true -FBXImporterSetParam "Cameras" true -FBXImporterSetParam "AxisConversionMethod" true -FbxExporterSetParam "UpAxis" "Y" -FbxExporterSetParam "Preserveinstances" true - -importFile @"{filepath}" #noPrompt using:FBXIMP - """) - - self.log.debug(f"Executing command: {fbx_import_cmd}") - rt.execute(fbx_import_cmd) + rt.FBXImporterSetParam("Animation", True) + rt.FBXImporterSetParam("Camera", True) + rt.FBXImporterSetParam("AxisConversionMethod", True) + rt.FBXImporterSetParam("UpAxis", "Y") + rt.FBXImporterSetParam("Preserveinstances", True) + rt.importFile( + filepath, + rt.name("noPrompt"), + using=rt.FBXIMP) container = rt.getNodeByName(f"{name}") if not container: diff --git a/openpype/hosts/max/plugins/load/load_model_fbx.py b/openpype/hosts/max/plugins/load/load_model_fbx.py index 7532e3a8a0..bf51e3b191 100644 --- a/openpype/hosts/max/plugins/load/load_model_fbx.py +++ b/openpype/hosts/max/plugins/load/load_model_fbx.py @@ -21,21 +21,15 @@ class FbxModelLoader(load.LoaderPlugin): from pymxs import runtime as rt filepath = os.path.normpath(self.fname) + rt.FBXImporterSetParam("Animation", False) + rt.FBXImporterSetParam("Cameras", False) + rt.FBXImporterSetParam("UpAxis", "Y") + rt.FBXImporterSetParam("Preserveinstances", True) + rt.importFile( + filepath, + rt.name("noPrompt"), + using=rt.FBXIMP) - fbx_import_cmd = ( - f""" - -FBXImporterSetParam "Animation" false -FBXImporterSetParam "Cameras" false -FBXImporterSetParam "AxisConversionMethod" true -FbxExporterSetParam "UpAxis" "Y" -FbxExporterSetParam "Preserveinstances" true - -importFile @"{filepath}" #noPrompt using:FBXIMP - """) - - self.log.debug(f"Executing command: {fbx_import_cmd}") - rt.execute(fbx_import_cmd) container = rt.getNodeByName(f"{name}") if not container: container = rt.container() From 6fd4cbf2998f6d486858d01472d6385c8f2bc809 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 18 May 2023 16:47:58 +0800 Subject: [PATCH 076/189] fix the axis conversion issue --- openpype/hosts/max/plugins/load/load_camera_fbx.py | 1 - openpype/hosts/max/plugins/load/load_model_fbx.py | 1 - 2 files changed, 2 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_camera_fbx.py b/openpype/hosts/max/plugins/load/load_camera_fbx.py index 9767321e3d..0c5dd762cf 100644 --- a/openpype/hosts/max/plugins/load/load_camera_fbx.py +++ b/openpype/hosts/max/plugins/load/load_camera_fbx.py @@ -23,7 +23,6 @@ class FbxLoader(load.LoaderPlugin): rt.FBXImporterSetParam("Animation", True) rt.FBXImporterSetParam("Camera", True) rt.FBXImporterSetParam("AxisConversionMethod", True) - rt.FBXImporterSetParam("UpAxis", "Y") rt.FBXImporterSetParam("Preserveinstances", True) rt.importFile( filepath, diff --git a/openpype/hosts/max/plugins/load/load_model_fbx.py b/openpype/hosts/max/plugins/load/load_model_fbx.py index bf51e3b191..01e6acae12 100644 --- a/openpype/hosts/max/plugins/load/load_model_fbx.py +++ b/openpype/hosts/max/plugins/load/load_model_fbx.py @@ -23,7 +23,6 @@ class FbxModelLoader(load.LoaderPlugin): filepath = os.path.normpath(self.fname) rt.FBXImporterSetParam("Animation", False) rt.FBXImporterSetParam("Cameras", False) - rt.FBXImporterSetParam("UpAxis", "Y") rt.FBXImporterSetParam("Preserveinstances", True) rt.importFile( filepath, From 60efa939a86196749a1dab1f5afe2fbdc01991dc Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 18 May 2023 10:59:13 +0200 Subject: [PATCH 077/189] OP-5714 - allow returning stub with not saved workfile (#4984) Without it is not possible to create first workile. --- openpype/hosts/aftereffects/api/pipeline.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/openpype/hosts/aftereffects/api/pipeline.py b/openpype/hosts/aftereffects/api/pipeline.py index 95f6f3235b..020022e263 100644 --- a/openpype/hosts/aftereffects/api/pipeline.py +++ b/openpype/hosts/aftereffects/api/pipeline.py @@ -60,9 +60,6 @@ class AfterEffectsHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): print("Not connected yet, ignoring") return - if not stub.get_active_document_name(): - return - self._stub = stub return self._stub From d95299a31b1d8aae602ac06993f57ce14dd397ee Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 18 May 2023 10:37:38 +0100 Subject: [PATCH 078/189] Added settings to make optional the setting the unit scale --- openpype/hosts/blender/api/pipeline.py | 27 ++++++++++++++---- .../defaults/project_settings/blender.json | 6 +++- .../schema_project_blender.json | 28 ++++++++++++++++--- 3 files changed, 50 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/blender/api/pipeline.py b/openpype/hosts/blender/api/pipeline.py index 02b1560e56..3618c1f4c8 100644 --- a/openpype/hosts/blender/api/pipeline.py +++ b/openpype/hosts/blender/api/pipeline.py @@ -124,19 +124,34 @@ def set_start_end_frames(): scene.render.resolution_y = resolution_y -def set_base_file_unit_scale(): +def on_new(): + set_start_end_frames() + project = os.environ.get("AVALON_PROJECT") settings = get_project_settings(project) - unit_scale = settings.get("blender").get("base_file_unit_scale") - - bpy.context.scene.unit_settings.scale_length = unit_scale + unit_scale_settings = settings.get("blender").get("unit_scale_settings") + unit_scale_enabled = unit_scale_settings.get("enabled") + if unit_scale_enabled: + unit_scale = unit_scale_settings.get("base_file_unit_scale") + bpy.context.scene.unit_settings.scale_length = unit_scale -def on_new(): +def on_open(): set_start_end_frames() - set_base_file_unit_scale() + project = os.environ.get("AVALON_PROJECT") + settings = get_project_settings(project) + + unit_scale_settings = settings.get("blender").get("unit_scale_settings") + unit_scale_enabled = unit_scale_settings.get("enabled") + apply_on_opening = unit_scale_settings.get("apply_on_opening") + if unit_scale_enabled and apply_on_opening: + unit_scale = unit_scale_settings.get("base_file_unit_scale") + prev_unit_scale = bpy.context.scene.unit_settings.scale_length + + if unit_scale != prev_unit_scale: + bpy.context.scene.unit_settings.scale_length = unit_scale def on_open(): set_start_end_frames() diff --git a/openpype/settings/defaults/project_settings/blender.json b/openpype/settings/defaults/project_settings/blender.json index 0b3f38a40f..41aebfa537 100644 --- a/openpype/settings/defaults/project_settings/blender.json +++ b/openpype/settings/defaults/project_settings/blender.json @@ -1,5 +1,9 @@ { - "base_file_unit_scale": 0.01, + "unit_scale_settings": { + "enabled": true, + "apply_on_opening": false, + "base_file_unit_scale": 0.01 + }, "imageio": { "ocio_config": { "enabled": false, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json index 00414b3210..0d0952a70a 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json @@ -6,10 +6,30 @@ "is_file": true, "children": [ { - "key": "base_file_unit_scale", - "type": "number", - "label": "Base File Unit Scale", - "decimal": 2 + "key": "unit_scale_settings", + "type": "dict", + "label": "Set Unit Scale", + "collapsible": true, + "is_group": true, + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "key": "apply_on_opening", + "type": "boolean", + "label": "Apply on Opening Existing Files" + }, + { + "key": "base_file_unit_scale", + "type": "number", + "label": "Base File Unit Scale", + "decimal": 2 + } + ] }, { "key": "imageio", From 0d4ae4efa7668ad5b5dc2a5a991b3e5cfcd92bf3 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 18 May 2023 10:38:22 +0100 Subject: [PATCH 079/189] Added message if base scale has been changed when opening a file --- openpype/hosts/blender/api/pipeline.py | 31 +++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/blender/api/pipeline.py b/openpype/hosts/blender/api/pipeline.py index 3618c1f4c8..9cc557c01a 100644 --- a/openpype/hosts/blender/api/pipeline.py +++ b/openpype/hosts/blender/api/pipeline.py @@ -85,6 +85,31 @@ def uninstall(): ops.unregister() +def show_message(title, message): + from openpype.widgets.message_window import Window + from .ops import BlenderApplication + + BlenderApplication.get_app() + + Window( + parent=None, + title=title, + message=message, + level="warning") + + +def message_window(title, message): + from .ops import ( + MainThreadItem, + execute_in_main_thread, + _process_app_events + ) + + mti = MainThreadItem(show_message, title, message) + execute_in_main_thread(mti) + _process_app_events() + + def set_start_end_frames(): project_name = legacy_io.active_project() asset_name = legacy_io.Session["AVALON_ASSET"] @@ -153,9 +178,9 @@ def on_open(): if unit_scale != prev_unit_scale: bpy.context.scene.unit_settings.scale_length = unit_scale -def on_open(): - set_start_end_frames() - set_base_file_unit_scale() + message_window( + "Base file unit scale changed", + "Base file unit scale changed to match the project settings.") @bpy.app.handlers.persistent From 87802d06203ad1cdad22d2aec5112fba1a04f0e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Thu, 18 May 2023 14:39:27 +0200 Subject: [PATCH 080/189] Fix: Download last workfile doesn't work if wf not already downloaded (#4942) --- openpype/modules/sync_server/sync_server.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/modules/sync_server/sync_server.py b/openpype/modules/sync_server/sync_server.py index d1d5c2863d..98065b68a0 100644 --- a/openpype/modules/sync_server/sync_server.py +++ b/openpype/modules/sync_server/sync_server.py @@ -237,8 +237,7 @@ def download_last_published_workfile( last_published_workfile_path = get_representation_path_with_anatomy( workfile_representation, anatomy ) - if (not last_published_workfile_path or - not os.path.exists(last_published_workfile_path)): + if not last_published_workfile_path: return # If representation isn't available on remote site, then return. From 8b68371e0c399b6e8b80f5300a77223bfefc7007 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 18 May 2023 14:33:26 +0100 Subject: [PATCH 081/189] Increased the number of decimals for the unit scale --- .../schemas/projects_schema/schema_project_blender.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json index 0d0952a70a..5b40169872 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json @@ -27,7 +27,7 @@ "key": "base_file_unit_scale", "type": "number", "label": "Base File Unit Scale", - "decimal": 2 + "decimal": 10 } ] }, From ec7c172fb2ec82d43ae014381829275581cf0ed2 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 18 May 2023 15:26:12 +0100 Subject: [PATCH 082/189] Implement loading of abc camera --- .../blender/plugins/load/load_camera_abc.py | 209 ++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 openpype/hosts/blender/plugins/load/load_camera_abc.py diff --git a/openpype/hosts/blender/plugins/load/load_camera_abc.py b/openpype/hosts/blender/plugins/load/load_camera_abc.py new file mode 100644 index 0000000000..21b48f409f --- /dev/null +++ b/openpype/hosts/blender/plugins/load/load_camera_abc.py @@ -0,0 +1,209 @@ +"""Load an asset in Blender from an Alembic file.""" + +from pathlib import Path +from pprint import pformat +from typing import Dict, List, Optional + +import bpy + +from openpype.pipeline import ( + get_representation_path, + AVALON_CONTAINER_ID, +) +from openpype.hosts.blender.api import plugin, lib +from openpype.hosts.blender.api.pipeline import ( + AVALON_CONTAINERS, + AVALON_PROPERTY, +) + + +class AbcCameraLoader(plugin.AssetLoader): + """Load a camera from Alembic file. + + Stores the imported asset in an empty named after the asset. + """ + + families = ["camera"] + representations = ["abc"] + + label = "Load Camera (ABC)" + icon = "code-fork" + color = "orange" + + def _remove(self, asset_group): + objects = list(asset_group.children) + + for obj in objects: + if obj.type == "CAMERA": + bpy.data.cameras.remove(obj.data) + elif obj.type == "EMPTY": + objects.extend(obj.children) + bpy.data.objects.remove(obj) + + def _process(self, libpath, asset_group, group_name): + plugin.deselect_all() + + bpy.ops.wm.alembic_import(filepath=libpath) + + objects = lib.get_selection() + + for obj in objects: + obj.parent = asset_group + + for obj in objects: + name = obj.name + obj.name = f"{group_name}:{name}" + if obj.type != "EMPTY": + name_data = obj.data.name + obj.data.name = f"{group_name}:{name_data}" + + if not obj.get(AVALON_PROPERTY): + obj[AVALON_PROPERTY] = dict() + + avalon_info = obj[AVALON_PROPERTY] + avalon_info.update({"container_name": group_name}) + + plugin.deselect_all() + + return objects + + def process_asset( + self, + context: dict, + name: str, + namespace: Optional[str] = None, + options: Optional[Dict] = None, + ) -> Optional[List]: + """ + Arguments: + name: Use pre-defined name + namespace: Use pre-defined namespace + context: Full parenthood of representation to load + options: Additional settings dictionary + """ + libpath = self.fname + asset = context["asset"]["name"] + subset = context["subset"]["name"] + + asset_name = plugin.asset_name(asset, subset) + unique_number = plugin.get_unique_number(asset, subset) + group_name = plugin.asset_name(asset, subset, unique_number) + namespace = namespace or f"{asset}_{unique_number}" + + avalon_container = bpy.data.collections.get(AVALON_CONTAINERS) + if not avalon_container: + avalon_container = bpy.data.collections.new(name=AVALON_CONTAINERS) + bpy.context.scene.collection.children.link(avalon_container) + + asset_group = bpy.data.objects.new(group_name, object_data=None) + avalon_container.objects.link(asset_group) + + objects = self._process(libpath, asset_group, group_name) + + objects = [] + nodes = list(asset_group.children) + + for obj in nodes: + objects.append(obj) + nodes.extend(list(obj.children)) + + bpy.context.scene.collection.objects.link(asset_group) + + asset_group[AVALON_PROPERTY] = { + "schema": "openpype:container-2.0", + "id": AVALON_CONTAINER_ID, + "name": name, + "namespace": namespace or "", + "loader": str(self.__class__.__name__), + "representation": str(context["representation"]["_id"]), + "libpath": libpath, + "asset_name": asset_name, + "parent": str(context["representation"]["parent"]), + "family": context["representation"]["context"]["family"], + "objectName": group_name, + } + + self[:] = objects + return objects + + def exec_update(self, container: Dict, representation: Dict): + """Update the loaded asset. + + This will remove all objects of the current collection, load the new + ones and add them to the collection. + If the objects of the collection are used in another collection they + will not be removed, only unlinked. Normally this should not be the + case though. + + Warning: + No nested collections are supported at the moment! + """ + object_name = container["objectName"] + asset_group = bpy.data.objects.get(object_name) + libpath = Path(get_representation_path(representation)) + extension = libpath.suffix.lower() + + self.log.info( + "Container: %s\nRepresentation: %s", + pformat(container, indent=2), + pformat(representation, indent=2), + ) + + assert asset_group, ( + f"The asset is not loaded: {container['objectName']}") + assert libpath, ( + f"No existing library file found for {container['objectName']}") + assert libpath.is_file(), f"The file doesn't exist: {libpath}" + assert extension in plugin.VALID_EXTENSIONS, ( + f"Unsupported file: {libpath}") + + metadata = asset_group.get(AVALON_PROPERTY) + group_libpath = metadata["libpath"] + + normalized_group_libpath = str( + Path(bpy.path.abspath(group_libpath)).resolve()) + normalized_libpath = str( + Path(bpy.path.abspath(str(libpath))).resolve()) + self.log.debug( + "normalized_group_libpath:\n %s\nnormalized_libpath:\n %s", + normalized_group_libpath, + normalized_libpath, + ) + if normalized_group_libpath == normalized_libpath: + self.log.info("Library already loaded, not updating...") + return + + mat = asset_group.matrix_basis.copy() + + self._remove(asset_group) + self._process(str(libpath), asset_group, object_name) + + asset_group.matrix_basis = mat + + metadata["libpath"] = str(libpath) + metadata["representation"] = str(representation["_id"]) + + def exec_remove(self, container: Dict) -> bool: + """Remove an existing container from a Blender scene. + + Arguments: + container (openpype:container-1.0): Container to remove, + from `host.ls()`. + + Returns: + bool: Whether the container was deleted. + + Warning: + No nested collections are supported at the moment! + """ + object_name = container["objectName"] + asset_group = bpy.data.objects.get(object_name) + + if not asset_group: + return False + + self._remove(asset_group) + + bpy.data.objects.remove(asset_group) + + return True From 51bba73ebe1801ae1c9443307277d6e0156783a6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 19 May 2023 17:13:53 +0200 Subject: [PATCH 083/189] Unreal: Addon Py2 compatibility (#4994) * fix python 2 compatibility of unreal addon * added a comment --- openpype/hosts/unreal/addon.py | 9 +++++---- openpype/hosts/unreal/lib.py | 1 - 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/unreal/addon.py b/openpype/hosts/unreal/addon.py index 9ded333d7d..1119b5c16c 100644 --- a/openpype/hosts/unreal/addon.py +++ b/openpype/hosts/unreal/addon.py @@ -1,8 +1,5 @@ import os -from pathlib import Path - from openpype.modules import IHostAddon, OpenPypeModule -from .lib import get_compatible_integration UNREAL_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -17,10 +14,14 @@ class UnrealAddon(OpenPypeModule, IHostAddon): def add_implementation_envs(self, env, app): """Modify environments to contain all required for implementation.""" # Set AYON_UNREAL_PLUGIN required for Unreal implementation + # Imports are in this method for Python 2 compatiblity of an addon + from pathlib import Path + + from .lib import get_compatible_integration ue_version = app.name.replace("-", ".") unreal_plugin_path = os.path.join( - UNREAL_ROOT_DIR, "integration", f"UE_{ue_version}", "Ayon" + UNREAL_ROOT_DIR, "integration", "UE_{}".format(ue_version), "Ayon" ) if not Path(unreal_plugin_path).exists(): compatible_versions = get_compatible_integration( diff --git a/openpype/hosts/unreal/lib.py b/openpype/hosts/unreal/lib.py index 821b4daecc..97771472cf 100644 --- a/openpype/hosts/unreal/lib.py +++ b/openpype/hosts/unreal/lib.py @@ -7,7 +7,6 @@ import json from typing import List -import openpype from distutils import dir_util import subprocess import re From 8de85e94a7e42cf12b26b6d813dc375bfb0485b5 Mon Sep 17 00:00:00 2001 From: Sponge96 Date: Fri, 19 May 2023 16:15:09 +0100 Subject: [PATCH 084/189] 3dsmax: Refactored publish plugins to use proper implementation of pymxs (#4988) * refactor: rt.execute(saveNodes) replaces with pymxs function - changed the function to no longer use the selection and instead feed it the nodes directly from get_all_children function. - removed maintained_seclection() as we're no longer overriding the selection of the Max scene. - black also used to format. * refactor: replaced rt.export string with proper pymxs implementation - black used for formatting - moved the general flow around as each function call is now seperate instead of large string * refactor: replaced rt.execute with pymxs implementation * refactor: replaced rt.execute with proper pymxs * refactor: replaced rt.execute where possible * fix: pymxs terrible argument handling - noPrompt doesn't seem to work unless you call rt.name and is also positional - using doesn't work as a string you need to feed it the actual rt object * fix: exportFile to use correct arguments * fix: rt.exportfile args * fix: exportfile args * refactor: replaced rt.execute with proper function * refactor: removed use of rt.execute and replaced with pymxs * refactor: removed configs from maintained_selection() block * refactor: updated black to be 79 charlines --- .../max/plugins/publish/extract_camera_abc.py | 50 ++++++---------- .../max/plugins/publish/extract_camera_fbx.py | 57 +++++++----------- .../plugins/publish/extract_max_scene_raw.py | 38 ++++-------- .../max/plugins/publish/extract_model.py | 54 +++++++---------- .../max/plugins/publish/extract_model_fbx.py | 59 ++++++++----------- .../max/plugins/publish/extract_model_obj.py | 37 ++++++------ .../max/plugins/publish/extract_pointcache.py | 38 +++++------- 7 files changed, 131 insertions(+), 202 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_camera_abc.py b/openpype/hosts/max/plugins/publish/extract_camera_abc.py index 8c23ff9878..6b3bb178a3 100644 --- a/openpype/hosts/max/plugins/publish/extract_camera_abc.py +++ b/openpype/hosts/max/plugins/publish/extract_camera_abc.py @@ -1,18 +1,11 @@ import os import pyblish.api -from openpype.pipeline import ( - publish, - OptionalPyblishPluginMixin -) +from openpype.pipeline import publish, OptionalPyblishPluginMixin from pymxs import runtime as rt -from openpype.hosts.max.api import ( - maintained_selection, - get_all_children -) +from openpype.hosts.max.api import maintained_selection, get_all_children -class ExtractCameraAlembic(publish.Extractor, - OptionalPyblishPluginMixin): +class ExtractCameraAlembic(publish.Extractor, OptionalPyblishPluginMixin): """ Extract Camera with AlembicExport """ @@ -38,38 +31,33 @@ class ExtractCameraAlembic(publish.Extractor, path = os.path.join(stagingdir, filename) # We run the render - self.log.info("Writing alembic '%s' to '%s'" % (filename, - stagingdir)) + self.log.info("Writing alembic '%s' to '%s'" % (filename, stagingdir)) - export_cmd = ( - f""" -AlembicExport.ArchiveType = #ogawa -AlembicExport.CoordinateSystem = #maya -AlembicExport.StartFrame = {start} -AlembicExport.EndFrame = {end} -AlembicExport.CustomAttributes = true - -exportFile @"{path}" #noPrompt selectedOnly:on using:AlembicExport - - """) - - self.log.debug(f"Executing command: {export_cmd}") + rt.AlembicExport.ArchiveType = rt.name("ogawa") + rt.AlembicExport.CoordinateSystem = rt.name("maya") + rt.AlembicExport.StartFrame = start + rt.AlembicExport.EndFrame = end + rt.AlembicExport.CustomAttributes = True with maintained_selection(): # select and export rt.select(get_all_children(rt.getNodeByName(container))) - rt.execute(export_cmd) + rt.exportFile( + path, + rt.name("noPrompt"), + selectedOnly=True, + using=rt.AlembicExport, + ) self.log.info("Performing Extraction ...") if "representations" not in instance.data: instance.data["representations"] = [] representation = { - 'name': 'abc', - 'ext': 'abc', - 'files': filename, + "name": "abc", + "ext": "abc", + "files": filename, "stagingDir": stagingdir, } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, - path)) + self.log.info("Extracted instance '%s' to: %s" % (instance.name, path)) diff --git a/openpype/hosts/max/plugins/publish/extract_camera_fbx.py b/openpype/hosts/max/plugins/publish/extract_camera_fbx.py index 7e92f355ed..4b4b349e19 100644 --- a/openpype/hosts/max/plugins/publish/extract_camera_fbx.py +++ b/openpype/hosts/max/plugins/publish/extract_camera_fbx.py @@ -1,18 +1,11 @@ import os import pyblish.api -from openpype.pipeline import ( - publish, - OptionalPyblishPluginMixin -) +from openpype.pipeline import publish, OptionalPyblishPluginMixin from pymxs import runtime as rt -from openpype.hosts.max.api import ( - maintained_selection, - get_all_children -) +from openpype.hosts.max.api import maintained_selection, get_all_children -class ExtractCameraFbx(publish.Extractor, - OptionalPyblishPluginMixin): +class ExtractCameraFbx(publish.Extractor, OptionalPyblishPluginMixin): """ Extract Camera with FbxExporter """ @@ -33,43 +26,35 @@ class ExtractCameraFbx(publish.Extractor, filename = "{name}.fbx".format(**instance.data) filepath = os.path.join(stagingdir, filename) - self.log.info("Writing fbx file '%s' to '%s'" % (filename, - filepath)) + self.log.info("Writing fbx file '%s' to '%s'" % (filename, filepath)) - # Need to export: - # Animation = True - # Cameras = True - # AxisConversionMethod - fbx_export_cmd = ( - f""" - -FBXExporterSetParam "Animation" true -FBXExporterSetParam "Cameras" true -FBXExporterSetParam "AxisConversionMethod" "Animation" -FbxExporterSetParam "UpAxis" "Y" -FbxExporterSetParam "Preserveinstances" true - -exportFile @"{filepath}" #noPrompt selectedOnly:true using:FBXEXP - - """) - - self.log.debug(f"Executing command: {fbx_export_cmd}") + rt.FBXExporterSetParam("Animation", True) + rt.FBXExporterSetParam("Cameras", True) + rt.FBXExporterSetParam("AxisConversionMethod", "Animation") + rt.FBXExporterSetParam("UpAxis", "Y") + rt.FBXExporterSetParam("Preserveinstances", True) with maintained_selection(): # select and export rt.select(get_all_children(rt.getNodeByName(container))) - rt.execute(fbx_export_cmd) + rt.exportFile( + filepath, + rt.name("noPrompt"), + selectedOnly=True, + using=rt.FBXEXP, + ) self.log.info("Performing Extraction ...") if "representations" not in instance.data: instance.data["representations"] = [] representation = { - 'name': 'fbx', - 'ext': 'fbx', - 'files': filename, + "name": "fbx", + "ext": "fbx", + "files": filename, "stagingDir": stagingdir, } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, - filepath)) + self.log.info( + "Extracted instance '%s' to: %s" % (instance.name, filepath) + ) diff --git a/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py b/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py index c14fcdbd0b..f0c2aff7f3 100644 --- a/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py +++ b/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py @@ -1,18 +1,11 @@ import os import pyblish.api -from openpype.pipeline import ( - publish, - OptionalPyblishPluginMixin -) +from openpype.pipeline import publish, OptionalPyblishPluginMixin from pymxs import runtime as rt -from openpype.hosts.max.api import ( - maintained_selection, - get_all_children -) +from openpype.hosts.max.api import get_all_children -class ExtractMaxSceneRaw(publish.Extractor, - OptionalPyblishPluginMixin): +class ExtractMaxSceneRaw(publish.Extractor, OptionalPyblishPluginMixin): """ Extract Raw Max Scene with SaveSelected """ @@ -20,9 +13,7 @@ class ExtractMaxSceneRaw(publish.Extractor, order = pyblish.api.ExtractorOrder - 0.2 label = "Extract Max Scene (Raw)" hosts = ["max"] - families = ["camera", - "maxScene", - "model"] + families = ["camera", "maxScene", "model"] optional = True def process(self, instance): @@ -37,26 +28,23 @@ class ExtractMaxSceneRaw(publish.Extractor, filename = "{name}.max".format(**instance.data) max_path = os.path.join(stagingdir, filename) - self.log.info("Writing max file '%s' to '%s'" % (filename, - max_path)) + self.log.info("Writing max file '%s' to '%s'" % (filename, max_path)) if "representations" not in instance.data: instance.data["representations"] = [] - # saving max scene - with maintained_selection(): - # need to figure out how to select the camera - rt.select(get_all_children(rt.getNodeByName(container))) - rt.execute(f'saveNodes selection "{max_path}" quiet:true') + nodes = get_all_children(rt.getNodeByName(container)) + rt.saveNodes(nodes, max_path, quiet=True) self.log.info("Performing Extraction ...") representation = { - 'name': 'max', - 'ext': 'max', - 'files': filename, + "name": "max", + "ext": "max", + "files": filename, "stagingDir": stagingdir, } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, - max_path)) + self.log.info( + "Extracted instance '%s' to: %s" % (instance.name, max_path) + ) diff --git a/openpype/hosts/max/plugins/publish/extract_model.py b/openpype/hosts/max/plugins/publish/extract_model.py index 710ad5f97d..4c7c98e2cc 100644 --- a/openpype/hosts/max/plugins/publish/extract_model.py +++ b/openpype/hosts/max/plugins/publish/extract_model.py @@ -1,18 +1,11 @@ import os import pyblish.api -from openpype.pipeline import ( - publish, - OptionalPyblishPluginMixin -) +from openpype.pipeline import publish, OptionalPyblishPluginMixin from pymxs import runtime as rt -from openpype.hosts.max.api import ( - maintained_selection, - get_all_children -) +from openpype.hosts.max.api import maintained_selection, get_all_children -class ExtractModel(publish.Extractor, - OptionalPyblishPluginMixin): +class ExtractModel(publish.Extractor, OptionalPyblishPluginMixin): """ Extract Geometry in Alembic Format """ @@ -36,39 +29,36 @@ class ExtractModel(publish.Extractor, filepath = os.path.join(stagingdir, filename) # We run the render - self.log.info("Writing alembic '%s' to '%s'" % (filename, - stagingdir)) + self.log.info("Writing alembic '%s' to '%s'" % (filename, stagingdir)) - export_cmd = ( - f""" -AlembicExport.ArchiveType = #ogawa -AlembicExport.CoordinateSystem = #maya -AlembicExport.CustomAttributes = true -AlembicExport.UVs = true -AlembicExport.VertexColors = true -AlembicExport.PreserveInstances = true - -exportFile @"{filepath}" #noPrompt selectedOnly:on using:AlembicExport - - """) - - self.log.debug(f"Executing command: {export_cmd}") + rt.AlembicExport.ArchiveType = rt.name("ogawa") + rt.AlembicExport.CoordinateSystem = rt.name("maya") + rt.AlembicExport.CustomAttributes = True + rt.AlembicExport.UVs = True + rt.AlembicExport.VertexColors = True + rt.AlembicExport.PreserveInstances = True with maintained_selection(): # select and export rt.select(get_all_children(rt.getNodeByName(container))) - rt.execute(export_cmd) + rt.exportFile( + filepath, + rt.name("noPrompt"), + selectedOnly=True, + using=rt.AlembicExport, + ) self.log.info("Performing Extraction ...") if "representations" not in instance.data: instance.data["representations"] = [] representation = { - 'name': 'abc', - 'ext': 'abc', - 'files': filename, + "name": "abc", + "ext": "abc", + "files": filename, "stagingDir": stagingdir, } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, - filepath)) + self.log.info( + "Extracted instance '%s' to: %s" % (instance.name, filepath) + ) diff --git a/openpype/hosts/max/plugins/publish/extract_model_fbx.py b/openpype/hosts/max/plugins/publish/extract_model_fbx.py index ce58e8cc17..e6ccb24cdd 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_fbx.py +++ b/openpype/hosts/max/plugins/publish/extract_model_fbx.py @@ -1,18 +1,11 @@ import os import pyblish.api -from openpype.pipeline import ( - publish, - OptionalPyblishPluginMixin -) +from openpype.pipeline import publish, OptionalPyblishPluginMixin from pymxs import runtime as rt -from openpype.hosts.max.api import ( - maintained_selection, - get_all_children -) +from openpype.hosts.max.api import maintained_selection, get_all_children -class ExtractModelFbx(publish.Extractor, - OptionalPyblishPluginMixin): +class ExtractModelFbx(publish.Extractor, OptionalPyblishPluginMixin): """ Extract Geometry in FBX Format """ @@ -33,42 +26,38 @@ class ExtractModelFbx(publish.Extractor, stagingdir = self.staging_dir(instance) filename = "{name}.fbx".format(**instance.data) - filepath = os.path.join(stagingdir, - filename) - self.log.info("Writing FBX '%s' to '%s'" % (filepath, - stagingdir)) + filepath = os.path.join(stagingdir, filename) + self.log.info("Writing FBX '%s' to '%s'" % (filepath, stagingdir)) - export_fbx_cmd = ( - f""" -FBXExporterSetParam "Animation" false -FBXExporterSetParam "Cameras" false -FBXExporterSetParam "Lights" false -FBXExporterSetParam "PointCache" false -FBXExporterSetParam "AxisConversionMethod" "Animation" -FbxExporterSetParam "UpAxis" "Y" -FbxExporterSetParam "Preserveinstances" true - -exportFile @"{filepath}" #noPrompt selectedOnly:true using:FBXEXP - - """) - - self.log.debug(f"Executing command: {export_fbx_cmd}") + rt.FBXExporterSetParam("Animation", False) + rt.FBXExporterSetParam("Cameras", False) + rt.FBXExporterSetParam("Lights", False) + rt.FBXExporterSetParam("PointCache", False) + rt.FBXExporterSetParam("AxisConversionMethod", "Animation") + rt.FBXExporterSetParam("UpAxis", "Y") + rt.FBXExporterSetParam("Preserveinstances", True) with maintained_selection(): # select and export rt.select(get_all_children(rt.getNodeByName(container))) - rt.execute(export_fbx_cmd) + rt.exportFile( + filepath, + rt.name("noPrompt"), + selectedOnly=True, + using=rt.FBXEXP, + ) self.log.info("Performing Extraction ...") if "representations" not in instance.data: instance.data["representations"] = [] representation = { - 'name': 'fbx', - 'ext': 'fbx', - 'files': filename, + "name": "fbx", + "ext": "fbx", + "files": filename, "stagingDir": stagingdir, } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, - filepath)) + self.log.info( + "Extracted instance '%s' to: %s" % (instance.name, filepath) + ) diff --git a/openpype/hosts/max/plugins/publish/extract_model_obj.py b/openpype/hosts/max/plugins/publish/extract_model_obj.py index 7bda237880..ed3d68c990 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_obj.py +++ b/openpype/hosts/max/plugins/publish/extract_model_obj.py @@ -1,18 +1,11 @@ import os import pyblish.api -from openpype.pipeline import ( - publish, - OptionalPyblishPluginMixin -) +from openpype.pipeline import publish, OptionalPyblishPluginMixin from pymxs import runtime as rt -from openpype.hosts.max.api import ( - maintained_selection, - get_all_children -) +from openpype.hosts.max.api import maintained_selection, get_all_children -class ExtractModelObj(publish.Extractor, - OptionalPyblishPluginMixin): +class ExtractModelObj(publish.Extractor, OptionalPyblishPluginMixin): """ Extract Geometry in OBJ Format """ @@ -33,27 +26,31 @@ class ExtractModelObj(publish.Extractor, stagingdir = self.staging_dir(instance) filename = "{name}.obj".format(**instance.data) - filepath = os.path.join(stagingdir, - filename) - self.log.info("Writing OBJ '%s' to '%s'" % (filepath, - stagingdir)) + filepath = os.path.join(stagingdir, filename) + self.log.info("Writing OBJ '%s' to '%s'" % (filepath, stagingdir)) with maintained_selection(): # select and export rt.select(get_all_children(rt.getNodeByName(container))) - rt.execute(f'exportFile @"{filepath}" #noPrompt selectedOnly:true using:ObjExp') # noqa + rt.exportFile( + filepath, + rt.name("noPrompt"), + selectedOnly=True, + using=rt.ObjExp, + ) self.log.info("Performing Extraction ...") if "representations" not in instance.data: instance.data["representations"] = [] representation = { - 'name': 'obj', - 'ext': 'obj', - 'files': filename, + "name": "obj", + "ext": "obj", + "files": filename, "stagingDir": stagingdir, } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, - filepath)) + self.log.info( + "Extracted instance '%s' to: %s" % (instance.name, filepath) + ) diff --git a/openpype/hosts/max/plugins/publish/extract_pointcache.py b/openpype/hosts/max/plugins/publish/extract_pointcache.py index 75d8a7972c..8658cecb1b 100644 --- a/openpype/hosts/max/plugins/publish/extract_pointcache.py +++ b/openpype/hosts/max/plugins/publish/extract_pointcache.py @@ -41,10 +41,7 @@ import os import pyblish.api from openpype.pipeline import publish from pymxs import runtime as rt -from openpype.hosts.max.api import ( - maintained_selection, - get_all_children -) +from openpype.hosts.max.api import maintained_selection, get_all_children class ExtractAlembic(publish.Extractor): @@ -66,35 +63,30 @@ class ExtractAlembic(publish.Extractor): path = os.path.join(parent_dir, file_name) # We run the render - self.log.info("Writing alembic '%s' to '%s'" % (file_name, - parent_dir)) + self.log.info("Writing alembic '%s' to '%s'" % (file_name, parent_dir)) - abc_export_cmd = ( - f""" -AlembicExport.ArchiveType = #ogawa -AlembicExport.CoordinateSystem = #maya -AlembicExport.StartFrame = {start} -AlembicExport.EndFrame = {end} - -exportFile @"{path}" #noPrompt selectedOnly:on using:AlembicExport - - """) - - self.log.debug(f"Executing command: {abc_export_cmd}") + rt.AlembicExport.ArchiveType = rt.name("ogawa") + rt.AlembicExport.CoordinateSystem = rt.name("maya") + rt.AlembicExport.StartFrame = start + rt.AlembicExport.EndFrame = end with maintained_selection(): # select and export - rt.select(get_all_children(rt.getNodeByName(container))) - rt.execute(abc_export_cmd) + rt.exportFile( + path, + rt.name("noPrompt"), + selectedOnly=True, + using=rt.AlembicExport, + ) if "representations" not in instance.data: instance.data["representations"] = [] representation = { - 'name': 'abc', - 'ext': 'abc', - 'files': file_name, + "name": "abc", + "ext": "abc", + "files": file_name, "stagingDir": parent_dir, } instance.data["representations"].append(representation) From eea816f0353b4dbcfed745061d83f61eb08cbcb2 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 20 May 2023 03:25:02 +0000 Subject: [PATCH 085/189] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index 954cfa945b..8874eb510d 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.8-nightly.1" +__version__ = "3.15.8-nightly.2" From 0b8400bc8e5785cc23239bc432b10a7cc3de4f4c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 20 May 2023 03:25:46 +0000 Subject: [PATCH 086/189] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 0f58d61881..244eb1a363 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.15.8-nightly.2 - 3.15.8-nightly.1 - 3.15.7 - 3.15.7-nightly.3 @@ -134,7 +135,6 @@ body: - 3.14.2-nightly.2 - 3.14.2-nightly.1 - 3.14.1 - - 3.14.1-nightly.4 validations: required: true - type: dropdown From e178244d46401e58fa398501e584e3a6360c0200 Mon Sep 17 00:00:00 2001 From: Ember Light <49758407+EmberLightVFX@users.noreply.github.com> Date: Mon, 22 May 2023 10:43:11 +0200 Subject: [PATCH 087/189] Fusion - Loader plugins updates (#4920) * Added get_bmd_library to acces BMD's internal python library * Added the option to import image and online families. + black formatted * Added workfile loader To import the content of another workfile into your current comp * Fixed wrong family and extension in workfile loader * black formatting * Added missing formats to fbx importer Fusions fbx importer can import a bunch of different formats other then fbx (confusing I know but it's how it is) * Update openpype/hosts/fusion/plugins/load/load_workfile.py Co-authored-by: Roy Nieterau --------- Co-authored-by: Roy Nieterau --- openpype/hosts/fusion/api/__init__.py | 1 + openpype/hosts/fusion/api/lib.py | 6 ++ .../hosts/fusion/plugins/load/load_fbx.py | 34 +++++-- .../fusion/plugins/load/load_sequence.py | 92 +++++++++++-------- .../fusion/plugins/load/load_workfile.py | 32 +++++++ 5 files changed, 118 insertions(+), 47 deletions(-) create mode 100644 openpype/hosts/fusion/plugins/load/load_workfile.py diff --git a/openpype/hosts/fusion/api/__init__.py b/openpype/hosts/fusion/api/__init__.py index 495fe286d5..dba55a98d9 100644 --- a/openpype/hosts/fusion/api/__init__.py +++ b/openpype/hosts/fusion/api/__init__.py @@ -13,6 +13,7 @@ from .lib import ( update_frame_range, set_asset_framerange, get_current_comp, + get_bmd_library, comp_lock_and_undo_chunk ) diff --git a/openpype/hosts/fusion/api/lib.py b/openpype/hosts/fusion/api/lib.py index 40cc4d2963..c33209823e 100644 --- a/openpype/hosts/fusion/api/lib.py +++ b/openpype/hosts/fusion/api/lib.py @@ -309,6 +309,12 @@ def get_fusion_module(): return fusion +def get_bmd_library(): + """Get bmd library""" + bmd = getattr(sys.modules["__main__"], "bmd", None) + return bmd + + def get_current_comp(): """Get current comp in this session""" fusion = get_fusion_module() diff --git a/openpype/hosts/fusion/plugins/load/load_fbx.py b/openpype/hosts/fusion/plugins/load/load_fbx.py index b8f501ae7e..c73ad78394 100644 --- a/openpype/hosts/fusion/plugins/load/load_fbx.py +++ b/openpype/hosts/fusion/plugins/load/load_fbx.py @@ -1,4 +1,3 @@ - from openpype.pipeline import ( load, get_representation_path, @@ -6,7 +5,7 @@ from openpype.pipeline import ( from openpype.hosts.fusion.api import ( imprint_container, get_current_comp, - comp_lock_and_undo_chunk + comp_lock_and_undo_chunk, ) @@ -15,7 +14,21 @@ class FusionLoadFBXMesh(load.LoaderPlugin): families = ["*"] representations = ["*"] - extensions = {"fbx"} + extensions = { + "3ds", + "amc", + "aoa", + "asf", + "bvh", + "c3d", + "dae", + "dxf", + "fbx", + "htr", + "mcd", + "obj", + "trc", + } label = "Load FBX mesh" order = -10 @@ -27,23 +40,24 @@ class FusionLoadFBXMesh(load.LoaderPlugin): def load(self, context, name, namespace, data): # Fallback to asset name when namespace is None if namespace is None: - namespace = context['asset']['name'] + namespace = context["asset"]["name"] # Create the Loader with the filename path set comp = get_current_comp() with comp_lock_and_undo_chunk(comp, "Create tool"): - path = self.fname args = (-32768, -32768) tool = comp.AddTool(self.tool_type, *args) tool["ImportFile"] = path - imprint_container(tool, - name=name, - namespace=namespace, - context=context, - loader=self.__class__.__name__) + imprint_container( + tool, + name=name, + namespace=namespace, + context=context, + loader=self.__class__.__name__, + ) def switch(self, container, representation): self.update(container, representation) diff --git a/openpype/hosts/fusion/plugins/load/load_sequence.py b/openpype/hosts/fusion/plugins/load/load_sequence.py index 38fd41c8b2..552e282587 100644 --- a/openpype/hosts/fusion/plugins/load/load_sequence.py +++ b/openpype/hosts/fusion/plugins/load/load_sequence.py @@ -3,17 +3,14 @@ import contextlib import openpype.pipeline.load as load from openpype.pipeline.load import ( get_representation_context, - get_representation_path_from_context + get_representation_path_from_context, ) from openpype.hosts.fusion.api import ( imprint_container, get_current_comp, - comp_lock_and_undo_chunk -) -from openpype.lib.transcoding import ( - IMAGE_EXTENSIONS, - VIDEO_EXTENSIONS + comp_lock_and_undo_chunk, ) +from openpype.lib.transcoding import IMAGE_EXTENSIONS, VIDEO_EXTENSIONS comp = get_current_comp() @@ -57,20 +54,23 @@ def preserve_trim(loader, log=None): try: yield finally: - length = loader.GetAttrs()["TOOLIT_Clip_Length"][1] - 1 if trim_from_start > length: trim_from_start = length if log: - log.warning("Reducing trim in to %d " - "(because of less frames)" % trim_from_start) + log.warning( + "Reducing trim in to %d " + "(because of less frames)" % trim_from_start + ) remainder = length - trim_from_start if trim_from_end > remainder: trim_from_end = remainder if log: - log.warning("Reducing trim in to %d " - "(because of less frames)" % trim_from_end) + log.warning( + "Reducing trim in to %d " + "(because of less frames)" % trim_from_end + ) loader["ClipTimeStart"][time] = trim_from_start loader["ClipTimeEnd"][time] = length - trim_from_end @@ -109,11 +109,15 @@ def loader_shift(loader, frame, relative=True): # 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 - with preserve_inputs(loader, inputs=["ClipTimeStart", - "ClipTimeEnd", - "HoldFirstFrame", - "HoldLastFrame"]): - + with preserve_inputs( + loader, + inputs=[ + "ClipTimeStart", + "ClipTimeEnd", + "HoldFirstFrame", + "HoldLastFrame", + ], + ): # GlobalIn cannot be set past GlobalOut or vice versa # so we must apply them in the order of the shift. if shift > 0: @@ -129,7 +133,14 @@ def loader_shift(loader, frame, relative=True): class FusionLoadSequence(load.LoaderPlugin): """Load image sequence into Fusion""" - families = ["imagesequence", "review", "render", "plate"] + families = [ + "imagesequence", + "review", + "render", + "plate", + "image", + "onilne", + ] representations = ["*"] extensions = set( ext.lstrip(".") for ext in IMAGE_EXTENSIONS.union(VIDEO_EXTENSIONS) @@ -143,7 +154,7 @@ class FusionLoadSequence(load.LoaderPlugin): def load(self, context, name, namespace, data): # Fallback to asset name when namespace is None if namespace is None: - namespace = context['asset']['name'] + namespace = context["asset"]["name"] # Use the first file for now path = get_representation_path_from_context(context) @@ -151,7 +162,6 @@ class FusionLoadSequence(load.LoaderPlugin): # Create the Loader with the filename path set comp = get_current_comp() with comp_lock_and_undo_chunk(comp, "Create Loader"): - args = (-32768, -32768) tool = comp.AddTool("Loader", *args) tool["Clip"] = path @@ -160,11 +170,13 @@ class FusionLoadSequence(load.LoaderPlugin): start = self._get_start(context["version"], tool) loader_shift(tool, start, relative=False) - imprint_container(tool, - name=name, - namespace=namespace, - context=context, - loader=self.__class__.__name__) + imprint_container( + tool, + name=name, + namespace=namespace, + context=context, + loader=self.__class__.__name__, + ) def switch(self, container, representation): self.update(container, representation) @@ -222,24 +234,28 @@ class FusionLoadSequence(load.LoaderPlugin): start = self._get_start(context["version"], tool) with comp_lock_and_undo_chunk(comp, "Update Loader"): - # Update the loader's path whilst preserving some values with preserve_trim(tool, log=self.log): - with preserve_inputs(tool, - inputs=("HoldFirstFrame", - "HoldLastFrame", - "Reverse", - "Depth", - "KeyCode", - "TimeCodeOffset")): + with preserve_inputs( + tool, + inputs=( + "HoldFirstFrame", + "HoldLastFrame", + "Reverse", + "Depth", + "KeyCode", + "TimeCodeOffset", + ), + ): tool["Clip"] = path # Set the global in to the start frame of the sequence global_in_changed = loader_shift(tool, start, relative=False) if global_in_changed: # Log this change to the user - self.log.debug("Changed '%s' global in: %d" % (tool.Name, - start)) + self.log.debug( + "Changed '%s' global in: %d" % (tool.Name, start) + ) # Update the imprinted representation tool.SetData("avalon.representation", str(representation["_id"])) @@ -264,9 +280,11 @@ class FusionLoadSequence(load.LoaderPlugin): # 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)) + 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 diff --git a/openpype/hosts/fusion/plugins/load/load_workfile.py b/openpype/hosts/fusion/plugins/load/load_workfile.py new file mode 100644 index 0000000000..b49d104a15 --- /dev/null +++ b/openpype/hosts/fusion/plugins/load/load_workfile.py @@ -0,0 +1,32 @@ +"""Import workfiles into your current comp. +As all imported nodes are free floating and will probably be changed there +is no update or reload function added for this plugin +""" + +from openpype.pipeline import load + +from openpype.hosts.fusion.api import ( + get_current_comp, + get_bmd_library, +) + + +class FusionLoadWorkfile(load.LoaderPlugin): + """Load the content of a workfile into Fusion""" + + families = ["workfile"] + representations = ["*"] + extensions = {"comp"} + + label = "Load Workfile" + order = -10 + icon = "code-fork" + color = "orange" + + def load(self, context, name, namespace, data): + # Get needed elements + bmd = get_bmd_library() + comp = get_current_comp() + + # Paste the content of the file into the current comp + comp.Paste(bmd.readfile(self.fname)) From 136af34a7189e78fd8549ffe029285283b7997c8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 22 May 2023 10:45:20 +0200 Subject: [PATCH 088/189] AfterEffects: set frame range and resolution (#4983) * OP-5660 - adding menu buttons to Set frame range in AE * OP-5660 - refactored location of scripts set_settings should be in lib as it is used elsewhere, but launch_logic and lib created circular dependency. Moved main to launch logic as it is actually handling launching. * OP-5660 - added set_settings to creator When instance gets created, set frame range and resolution from DB. * OP-5660 - minor fix * OP-5660 - updated extension zip * OP-5660 - updated documentation * OP-5660 - fixed missing exception * OP-5660 - fixed argument * OP-5560 - fix imports * OP-5660 - updated extension * OP-5660 - add js alert message for buttons * OP-5660 - repacked extension Without Anastasyi showed success, but extension wasn't loaded. * OP-5660 - make log message nicer * OP-5660 - added log if workfile not saved * OP-5660 - provide defaults to limit None exception * OP-5660 - updated error message --- openpype/hosts/aftereffects/api/__init__.py | 10 +- openpype/hosts/aftereffects/api/extension.zxp | Bin 101426 -> 101866 bytes .../api/extension/CSXS/manifest.xml | 4 +- .../aftereffects/api/extension/index.html | 72 +++++++--- .../aftereffects/api/extension/js/main.js | 56 +++++--- .../api/extension/jsx/hostscript.jsx | 134 ++++++++++++------ .../hosts/aftereffects/api/launch_logic.py | 86 ++++++++--- openpype/hosts/aftereffects/api/lib.py | 118 ++++++++------- openpype/hosts/aftereffects/api/pipeline.py | 6 +- openpype/hosts/aftereffects/api/ws_stub.py | 65 ++++++--- .../plugins/create/create_render.py | 19 ++- .../plugins/publish/collect_render.py | 16 +-- openpype/scripts/non_python_host_launch.py | 3 +- website/docs/artist_hosts_aftereffects.md | 37 ++++- .../docs/assets/aftereffects_extension.png | Bin 0 -> 12533 bytes 15 files changed, 414 insertions(+), 212 deletions(-) create mode 100644 website/docs/assets/aftereffects_extension.png diff --git a/openpype/hosts/aftereffects/api/__init__.py b/openpype/hosts/aftereffects/api/__init__.py index a7137ba8fb..28062cc35d 100644 --- a/openpype/hosts/aftereffects/api/__init__.py +++ b/openpype/hosts/aftereffects/api/__init__.py @@ -4,9 +4,8 @@ Anything that isn't defined here is INTERNAL and unreliable for external use. """ -from .launch_logic import ( +from .ws_stub import ( get_stub, - stub, ) from .pipeline import ( @@ -18,7 +17,8 @@ from .pipeline import ( from .lib import ( maintained_selection, get_extension_manifest_path, - get_asset_settings + get_asset_settings, + set_settings ) from .plugin import ( @@ -27,9 +27,8 @@ from .plugin import ( __all__ = [ - # launch_logic + # ws_stub "get_stub", - "stub", # pipeline "ls", @@ -39,6 +38,7 @@ __all__ = [ "maintained_selection", "get_extension_manifest_path", "get_asset_settings", + "set_settings", # plugin "AfterEffectsLoader" diff --git a/openpype/hosts/aftereffects/api/extension.zxp b/openpype/hosts/aftereffects/api/extension.zxp index b436f0ca0b67313cf4509d1ec5e7d4a1dfd2d106..50fda416f806515e8a6e90e745153e341f73c7b2 100644 GIT binary patch delta 14164 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 XY6T7RTV3K`-(5HW;4}pI>(~DQ$dj(< delta 13616 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 - + diff --git a/openpype/hosts/aftereffects/api/extension/index.html b/openpype/hosts/aftereffects/api/extension/index.html index 52a7c4964f..291965559f 100644 --- a/openpype/hosts/aftereffects/api/extension/index.html +++ b/openpype/hosts/aftereffects/api/extension/index.html @@ -2,7 +2,7 @@ - + @@ -25,11 +25,11 @@ - + - + - + - + - + + + + + + + - - + + + - - + @@ -107,6 +143,6 @@ - + - \ No newline at end of file + diff --git a/openpype/hosts/aftereffects/api/extension/js/main.js b/openpype/hosts/aftereffects/api/extension/js/main.js index bb0f3b1f0c..ffc41f0937 100644 --- a/openpype/hosts/aftereffects/api/extension/js/main.js +++ b/openpype/hosts/aftereffects/api/extension/js/main.js @@ -4,7 +4,7 @@ indent: 4, maxerr: 50 */ var csInterface = new CSInterface(); - + log.warn("script start"); WSRPC.DEBUG = false; @@ -14,7 +14,7 @@ WSRPC.TRACE = false; async function startUp(url){ promis = runEvalScript("getEnv('" + url + "')"); - var res = await promis; + var res = await promis; log.warn("res: " + res); promis = runEvalScript("getEnv('OPENPYPE_DEBUG')"); @@ -56,7 +56,7 @@ function get_extension_version(){ } function main(websocket_url){ - // creates connection to 'websocket_url', registers routes + // creates connection to 'websocket_url', registers routes var default_url = 'ws://localhost:8099/ws/'; if (websocket_url == ''){ @@ -66,7 +66,7 @@ function main(websocket_url){ RPC.connect(); - log.warn("connected"); + log.warn("connected"); RPC.addRoute('AfterEffects.open', function (data) { log.warn('Server called client route "open":', data); @@ -88,7 +88,7 @@ function main(websocket_url){ }); RPC.addRoute('AfterEffects.get_active_document_name', function (data) { - log.warn('Server called client route ' + + log.warn('Server called client route ' + '"get_active_document_name":', data); return runEvalScript("getActiveDocumentName()") .then(function(result){ @@ -98,7 +98,7 @@ function main(websocket_url){ }); RPC.addRoute('AfterEffects.get_active_document_full_name', function (data){ - log.warn('Server called client route ' + + log.warn('Server called client route ' + '"get_active_document_full_name":', data); return runEvalScript("getActiveDocumentFullName()") .then(function(result){ @@ -118,7 +118,7 @@ function main(websocket_url){ }); }); - + RPC.addRoute('AfterEffects.get_selected_items', function (data) { log.warn('Server called client route "get_selected_items":', data); return runEvalScript("getSelectedItems(" + data.comps + "," + @@ -194,23 +194,25 @@ function main(websocket_url){ }); }); - RPC.addRoute('AfterEffects.get_work_area', function (data) { - log.warn('Server called client route "get_work_area":', data); - return runEvalScript("getWorkArea(" + data.item_id + ")") + RPC.addRoute('AfterEffects.get_comp_properties', function (data) { + log.warn('Server called client route "get_comp_properties":', data); + return runEvalScript("getCompProperties(" + data.item_id + ")") .then(function(result){ - log.warn("getWorkArea: " + result); + log.warn("get_comp_properties: " + result); return result; }); }); - RPC.addRoute('AfterEffects.set_work_area', function (data) { + RPC.addRoute('AfterEffects.set_comp_properties', function (data) { log.warn('Server called client route "set_work_area":', data); - return runEvalScript("setWorkArea(" + data.item_id + ',' + + return runEvalScript("setCompProperties(" + data.item_id + ',' + data.start + ',' + data.duration + ',' + - data.frame_rate + ")") + data.frame_rate + ',' + + data.width + ',' + + data.height + ")") .then(function(result){ - log.warn("getWorkArea: " + result); + log.warn("set_comp_properties: " + result); return result; }); }); @@ -255,7 +257,7 @@ function main(websocket_url){ RPC.addRoute('AfterEffects.import_background', function (data) { log.warn('Server called client route "import_background":', data); - return runEvalScript("importBackground(" + data.comp_id + ", " + + return runEvalScript("importBackground(" + data.comp_id + ", " + "'" + data.comp_name + "', " + JSON.stringify(data.files) + ")") .then(function(result){ @@ -266,7 +268,7 @@ function main(websocket_url){ RPC.addRoute('AfterEffects.reload_background', function (data) { log.warn('Server called client route "reload_background":', data); - return runEvalScript("reloadBackground(" + data.comp_id + ", " + + return runEvalScript("reloadBackground(" + data.comp_id + ", " + "'" + data.comp_name + "', " + JSON.stringify(data.files) + ")") .then(function(result){ @@ -314,6 +316,16 @@ function main(websocket_url){ log.warn('Server called client route "close":', data); return runEvalScript("close()"); }); + + RPC.addRoute('AfterEffects.print_msg', function (data) { + log.warn('Server called client route "print_msg":', data); + var escaped_msg = EscapeStringForJSX(data.msg); + return runEvalScript("printMsg('" + escaped_msg +"')") + .then(function(result){ + log.warn("print_msg: " + result); + return result; + }); + }); } /** main entry point **/ @@ -323,17 +335,17 @@ startUp("WEBSOCKET_URL"); 'use strict'; var csInterface = new CSInterface(); - - + + function init() { - + themeManager.init(); - + $("#btn_test").click(function () { csInterface.evalScript('sayHello()'); }); } - + init(); }()); diff --git a/openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx b/openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx index 5c1d163439..7d0b20bbb4 100644 --- a/openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx +++ b/openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx @@ -1,7 +1,7 @@ /*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */ /*global $, Folder*/ -#include "../js/libs/json.js"; +//@include "../js/libs/json.js" /* All public API function should return JSON! */ @@ -29,13 +29,13 @@ function getEnv(variable){ function getMetadata(){ /** * Returns payload in 'Label' field of project's metadata - * + * **/ if (ExternalObject.AdobeXMPScript === undefined){ ExternalObject.AdobeXMPScript = new ExternalObject('lib:AdobeXMPScript'); } - + var proj = app.project; var meta = new XMPMeta(app.project.xmpPacket); var schemaNS = XMPMeta.getNamespaceURI("xmp"); @@ -53,7 +53,7 @@ function getMetadata(){ function imprint(payload){ /** * Stores payload in 'Label' field of project's metadata - * + * * Args: * payload (string): json content */ @@ -61,14 +61,14 @@ function imprint(payload){ ExternalObject.AdobeXMPScript = new ExternalObject('lib:AdobeXMPScript'); } - + var proj = app.project; var meta = new XMPMeta(app.project.xmpPacket); var schemaNS = XMPMeta.getNamespaceURI("xmp"); var label = "xmp:Label"; meta.setProperty(schemaNS, label, payload); - + app.project.xmpPacket = meta.serialize(); } @@ -116,14 +116,14 @@ function getItems(comps, folders, footages){ /** * Returns JSON representation of compositions and * if 'collectLayers' then layers in comps too. - * + * * Args: * comps (bool): return selected compositions * folders (bool): return folders * footages (bool): return FootageItem * Returns: * (list) of JSON items - */ + */ var items = [] for (i = 1; i <= app.project.items.length; ++i){ var item = app.project.items[i]; @@ -142,14 +142,14 @@ function getItems(comps, folders, footages){ function getSelectedItems(comps, folders, footages){ /** * Returns list of selected items from Project menu - * + * * Args: * comps (bool): return selected compositions * folders (bool): return folders * footages (bool): return FootageItem * Returns: * (list) of JSON items - */ + */ var items = [] for (i = 0; i < app.project.selection.length; ++i){ var item = app.project.selection[i]; @@ -166,9 +166,9 @@ function getSelectedItems(comps, folders, footages){ function _getItem(item, comps, folders, footages){ /** - * Auxiliary function as project items and selections + * Auxiliary function as project items and selections * are indexed in different way :/ - * Refactor + * Refactor */ var item_type = ''; if (item instanceof FolderItem){ @@ -189,7 +189,7 @@ function _getItem(item, comps, folders, footages){ return "{}"; } } - + var item = {"name": item.name, "id": item.id, "type": item_type}; @@ -200,7 +200,7 @@ function importFile(path, item_name, import_options){ /** * Imports file (image tested for now) as a FootageItem. * Creates new composition - * + * * Args: * path (string): absolute path to image file * item_name (string): label for composition @@ -218,7 +218,7 @@ function importFile(path, item_name, import_options){ app.beginUndoGroup("Import File"); fp = new File(path); if (fp.exists){ - try { + try { im_opt = new ImportOptions(fp); importAsType = import_options["ImportAsType"]; @@ -234,18 +234,18 @@ function importFile(path, item_name, import_options){ } if (importAsType.indexOf('PROJECT') > 0){ im_opt.importAs = ImportAsType.PROJECT; - } - + } + } if ('sequence' in import_options){ im_opt.sequence = true; } - + comp = app.project.importFile(im_opt); if (app.project.selection.length == 2 && app.project.selection[0] instanceof FolderItem){ - comp.parentFolder = app.project.selection[0] + comp.parentFolder = app.project.selection[0] } } catch (error) { return _prepareError(error.toString() + importOptions.file.fsName); @@ -283,14 +283,14 @@ function setLabelColor(comp_id, color_idx){ function replaceItem(comp_id, path, item_name){ /** * Replaces loaded file with new file and updates name - * + * * Args: * comp_id (int): id of composition, not a index! * path (string): absolute path to new file * item_name (string): new composition name */ app.beginUndoGroup("Replace File"); - + fp = new File(path); if (!fp.exists){ return _prepareError("File " + path + " not found."); @@ -303,7 +303,7 @@ function replaceItem(comp_id, path, item_name){ }else{ item.replace(fp); } - + item.name = item_name; } catch (error) { return _prepareError(error.toString() + path); @@ -319,7 +319,7 @@ function replaceItem(comp_id, path, item_name){ function renameItem(item_id, new_name){ /** * Renames item with 'item_id' to 'new_name' - * + * * Args: * item_id (int): id to search item * new_name (str) @@ -335,7 +335,7 @@ function renameItem(item_id, new_name){ function deleteItem(item_id){ /** * Delete any 'item_id' - * + * * Not restricted only to comp, it could delete * any item with 'id' */ @@ -347,38 +347,76 @@ function deleteItem(item_id){ } } -function getWorkArea(comp_id){ +function getCompProperties(comp_id){ /** - * Returns information about workarea - are that will be - * rendered. All calculation will be done in OpenPype, - * easier to modify without redeploy of extension. - * + * Returns information about composition - are that will be + * rendered. + * * Returns * (dict) */ - var item = app.project.itemByID(comp_id); - if (item){ - return JSON.stringify({ - "workAreaStart": item.displayStartFrame, - "workAreaDuration": item.duration, - "frameRate": item.frameRate}); - }else{ + var comp = app.project.itemByID(comp_id); + if (!comp){ return _prepareError("There is no composition with "+ comp_id); } + + return JSON.stringify({ + "id": comp.id, + "name": comp.name, + "frameStart": comp.displayStartFrame, + "framesDuration": comp.duration * comp.frameRate, + "frameRate": comp.frameRate, + "width": comp.width, + "height": comp.height}); } -function setWorkArea(comp_id, workAreaStart, workAreaDuration, frameRate){ +function setCompProperties(comp_id, frameStart, framesCount, frameRate, + width, height){ /** * Sets work area info from outside (from Ftrack via OpenPype) */ - var item = app.project.itemByID(comp_id); - if (item){ - item.displayStartTime = workAreaStart; - item.duration = workAreaDuration; - item.frameRate = frameRate; - }else{ + var comp = app.project.itemByID(comp_id); + if (!comp){ return _prepareError("There is no composition with "+ comp_id); } + + app.beginUndoGroup('change comp properties'); + if (frameStart && framesCount && frameRate){ + comp.displayStartFrame = frameStart; + comp.duration = framesCount / frameRate; + comp.frameRate = frameRate; + } + if (width && height){ + var widthOld = comp.width; + var widthNew = width; + var widthDelta = widthNew - widthOld; + + var heightOld = comp.height; + var heightNew = height; + var heightDelta = heightNew - heightOld; + + var offset = [widthDelta / 2, heightDelta / 2]; + + comp.width = widthNew; + comp.height = heightNew; + + for (var i = 1, il = comp.numLayers; i <= il; i++) { + var layer = comp.layer(i); + var positionProperty = layer.property('ADBE Transform Group').property('ADBE Position'); + + if (positionProperty.numKeys > 0) { + for (var j = 1, jl = positionProperty.numKeys; j <= jl; j++) { + var keyValue = positionProperty.keyValue(j); + positionProperty.setValueAtKey(j, keyValue + offset); + } + } else { + var positionValue = positionProperty.value; + positionProperty.setValue(positionValue + offset); + } + } + } + + app.endUndoGroup(); } function save(){ @@ -504,7 +542,7 @@ function addItemAsLayerToComp(comp_id, item_id, found_comp){ * Args: * comp_id (int): id of target composition * item_id (int): FootageItem.id - * found_comp (CompItem, optional): to limit querying if + * found_comp (CompItem, optional): to limit quering if * comp already found previously */ var comp = found_comp || app.project.itemByID(comp_id); @@ -749,7 +787,7 @@ function render(target_folder, comp_id){ var om1 = app.project.renderQueue.item(i).outputModule(1); var file_name = File.decode( om1.file.name ).replace('β„—', ''); // Name contains special character, space? - + var omItem1_settable_str = app.project.renderQueue.item(i).outputModule(1).getSettings( GetSettingsFormat.STRING_SETTABLE ); var targetFolder = new Folder(target_folder); @@ -763,7 +801,7 @@ function render(target_folder, comp_id){ render_item.render = false; } } - + } app.beginSuppressDialogs(); app.project.renderQueue.render(); @@ -779,6 +817,10 @@ function getAppVersion(){ return _prepareSingleValue(app.version); } +function printMsg(msg){ + alert(msg); +} + function _prepareSingleValue(value){ return JSON.stringify({"result": value}) } diff --git a/openpype/hosts/aftereffects/api/launch_logic.py b/openpype/hosts/aftereffects/api/launch_logic.py index c428043d99..77c2b0b6ca 100644 --- a/openpype/hosts/aftereffects/api/launch_logic.py +++ b/openpype/hosts/aftereffects/api/launch_logic.py @@ -1,49 +1,77 @@ import os +import sys import subprocess import collections import logging import asyncio import functools +import traceback + from wsrpc_aiohttp import ( WebSocketRoute, WebSocketAsync ) -from qtpy import QtCore +from qtpy import QtCore, QtWidgets from openpype.lib import Logger -from openpype.pipeline import legacy_io from openpype.tools.utils import host_tools +from openpype.tests.lib import is_in_tests +from openpype.pipeline import install_host, legacy_io +from openpype.modules import ModulesManager from openpype.tools.adobe_webserver.app import WebServerTool -from .ws_stub import AfterEffectsServerStub +from .ws_stub import get_stub +from .lib import set_settings log = logging.getLogger(__name__) log.setLevel(logging.DEBUG) -class ConnectionNotEstablishedYet(Exception): - pass +def safe_excepthook(*args): + traceback.print_exception(*args) -def get_stub(): - """ - Convenience function to get server RPC stub to call methods directed - for host (Photoshop). - It expects already created connection, started from client. - Currently created when panel is opened (PS: Window>Extensions>Avalon) - :return: where functions could be called from - """ - ae_stub = AfterEffectsServerStub() - if not ae_stub.client: - raise ConnectionNotEstablishedYet("Connection is not created yet") +def main(*subprocess_args): + """Main entrypoint to AE launching, called from pre hook.""" + sys.excepthook = safe_excepthook - return ae_stub + from openpype.hosts.aftereffects.api import AfterEffectsHost + host = AfterEffectsHost() + install_host(host) -def stub(): - return get_stub() + os.environ["OPENPYPE_LOG_NO_COLORS"] = "False" + app = QtWidgets.QApplication([]) + app.setQuitOnLastWindowClosed(False) + + launcher = ProcessLauncher(subprocess_args) + launcher.start() + + if os.environ.get("HEADLESS_PUBLISH"): + manager = ModulesManager() + webpublisher_addon = manager["webpublisher"] + + launcher.execute_in_main_thread( + functools.partial( + webpublisher_addon.headless_publish, + log, + "CloseAE", + is_in_tests() + ) + ) + + elif os.environ.get("AVALON_PHOTOSHOP_WORKFILES_ON_LAUNCH", True): + save = False + if os.getenv("WORKFILES_SAVE_AS"): + save = True + + launcher.execute_in_main_thread( + lambda: host_tools.show_tool_by_name("workfiles", save=save) + ) + + sys.exit(app.exec_()) def show_tool_by_name(tool_name): @@ -55,6 +83,7 @@ def show_tool_by_name(tool_name): class ProcessLauncher(QtCore.QObject): + """Launches webserver, connects to it, runs main thread.""" route_name = "AfterEffects" _main_thread_callbacks = collections.deque() @@ -296,6 +325,15 @@ class AfterEffectsRoute(WebSocketRoute): async def sceneinventory_route(self): self._tool_route("sceneinventory") + async def setresolution_route(self): + self._settings_route(False, True) + + async def setframes_route(self): + self._settings_route(True, False) + + async def setall_route(self): + self._settings_route(True, True) + async def experimental_tools_route(self): self._tool_route("experimental_tools") @@ -309,3 +347,13 @@ class AfterEffectsRoute(WebSocketRoute): # Required return statement. return "nothing" + + def _settings_route(self, frames, resolution): + partial_method = functools.partial(set_settings, + frames, + resolution) + + ProcessLauncher.execute_in_main_thread(partial_method) + + # Required return statement. + return "nothing" diff --git a/openpype/hosts/aftereffects/api/lib.py b/openpype/hosts/aftereffects/api/lib.py index a39af5c81f..e8352c382b 100644 --- a/openpype/hosts/aftereffects/api/lib.py +++ b/openpype/hosts/aftereffects/api/lib.py @@ -1,69 +1,17 @@ import os -import sys import re import json import contextlib -import traceback import logging -from functools import partial -from qtpy import QtWidgets - -from openpype.pipeline import install_host -from openpype.modules import ModulesManager - -from openpype.tools.utils import host_tools -from openpype.tests.lib import is_in_tests -from .launch_logic import ProcessLauncher, get_stub +from openpype.pipeline.context_tools import get_current_context +from openpype.client import get_asset_by_name +from .ws_stub import get_stub log = logging.getLogger(__name__) log.setLevel(logging.DEBUG) -def safe_excepthook(*args): - traceback.print_exception(*args) - - -def main(*subprocess_args): - sys.excepthook = safe_excepthook - - from openpype.hosts.aftereffects.api import AfterEffectsHost - - host = AfterEffectsHost() - install_host(host) - - os.environ["OPENPYPE_LOG_NO_COLORS"] = "False" - app = QtWidgets.QApplication([]) - app.setQuitOnLastWindowClosed(False) - - launcher = ProcessLauncher(subprocess_args) - launcher.start() - - if os.environ.get("HEADLESS_PUBLISH"): - manager = ModulesManager() - webpublisher_addon = manager["webpublisher"] - - launcher.execute_in_main_thread( - partial( - webpublisher_addon.headless_publish, - log, - "CloseAE", - is_in_tests() - ) - ) - - elif os.environ.get("AVALON_PHOTOSHOP_WORKFILES_ON_LAUNCH", True): - save = False - if os.getenv("WORKFILES_SAVE_AS"): - save = True - - launcher.execute_in_main_thread( - lambda: host_tools.show_tool_by_name("workfiles", save=save) - ) - - sys.exit(app.exec_()) - - @contextlib.contextmanager def maintained_selection(): """Maintain selection during context.""" @@ -145,13 +93,13 @@ def get_asset_settings(asset_doc): """ asset_data = asset_doc["data"] - fps = asset_data.get("fps") - frame_start = asset_data.get("frameStart") - frame_end = asset_data.get("frameEnd") - handle_start = asset_data.get("handleStart") - handle_end = asset_data.get("handleEnd") - resolution_width = asset_data.get("resolutionWidth") - resolution_height = asset_data.get("resolutionHeight") + fps = asset_data.get("fps", 0) + frame_start = asset_data.get("frameStart", 0) + frame_end = asset_data.get("frameEnd", 0) + handle_start = asset_data.get("handleStart", 0) + handle_end = asset_data.get("handleEnd", 0) + resolution_width = asset_data.get("resolutionWidth", 0) + resolution_height = asset_data.get("resolutionHeight", 0) duration = (frame_end - frame_start + 1) + handle_start + handle_end return { @@ -164,3 +112,49 @@ def get_asset_settings(asset_doc): "resolutionHeight": resolution_height, "duration": duration } + + +def set_settings(frames, resolution, comp_ids=None, print_msg=True): + """Sets number of frames and resolution to selected comps. + + Args: + frames (bool): True if set frame info + resolution (bool): True if set resolution + comp_ids (list): specific composition ids, if empty + it tries to look for currently selected + print_msg (bool): True throw JS alert with msg + """ + frame_start = frames_duration = fps = width = height = None + current_context = get_current_context() + + asset_doc = get_asset_by_name(current_context["project_name"], + current_context["asset_name"]) + settings = get_asset_settings(asset_doc) + + msg = '' + if frames: + frame_start = settings["frameStart"] - settings["handleStart"] + frames_duration = settings["duration"] + fps = settings["fps"] + msg += f"frame start:{frame_start}, duration:{frames_duration}, "\ + f"fps:{fps}" + if resolution: + width = settings["resolutionWidth"] + height = settings["resolutionHeight"] + msg += f"width:{width} and height:{height}" + + stub = get_stub() + if not comp_ids: + comps = stub.get_selected_items(True, False, False) + comp_ids = [comp.id for comp in comps] + if not comp_ids: + stub.print_msg("Select at least one composition to apply settings.") + return + + for comp_id in comp_ids: + msg = f"Setting for comp {comp_id} " + msg + log.debug(msg) + stub.set_comp_properties(comp_id, frame_start, frames_duration, + fps, width, height) + if print_msg: + stub.print_msg(msg) diff --git a/openpype/hosts/aftereffects/api/pipeline.py b/openpype/hosts/aftereffects/api/pipeline.py index 020022e263..27aee8c7ce 100644 --- a/openpype/hosts/aftereffects/api/pipeline.py +++ b/openpype/hosts/aftereffects/api/pipeline.py @@ -8,10 +8,7 @@ from openpype.lib import Logger, register_event_callback from openpype.pipeline import ( register_loader_plugin_path, register_creator_plugin_path, - deregister_loader_plugin_path, - deregister_creator_plugin_path, AVALON_CONTAINER_ID, - legacy_io, ) from openpype.pipeline.load import any_outdated_containers import openpype.hosts.aftereffects @@ -23,7 +20,8 @@ from openpype.host import ( IPublishHost ) -from .launch_logic import get_stub, ConnectionNotEstablishedYet +from .launch_logic import get_stub +from .ws_stub import ConnectionNotEstablishedYet log = Logger.get_logger(__name__) diff --git a/openpype/hosts/aftereffects/api/ws_stub.py b/openpype/hosts/aftereffects/api/ws_stub.py index f094c7fa2a..576c997f49 100644 --- a/openpype/hosts/aftereffects/api/ws_stub.py +++ b/openpype/hosts/aftereffects/api/ws_stub.py @@ -11,6 +11,10 @@ from wsrpc_aiohttp import WebSocketAsync from openpype.tools.adobe_webserver.app import WebServerTool +class ConnectionNotEstablishedYet(Exception): + pass + + @attr.s class AEItem(object): """ @@ -24,8 +28,8 @@ class AEItem(object): # all imported elements, single for # regular image, array for Backgrounds members = attr.ib(factory=list) - workAreaStart = attr.ib(default=None) - workAreaDuration = attr.ib(default=None) + frameStart = attr.ib(default=None) + framesDuration = attr.ib(default=None) frameRate = attr.ib(default=None) file_name = attr.ib(default=None) instance_id = attr.ib(default=None) # New Publisher @@ -355,42 +359,50 @@ class AfterEffectsServerStub(): return self._handle_return(res) - def get_work_area(self, item_id): - """ Get work are information for render purposes + def get_comp_properties(self, comp_id): + """ Get composition information for render purposes + + Returns startFrame, frameDuration, fps, width, height. + Args: - item_id (int): + comp_id (int): Returns: (AEItem) """ res = self.websocketserver.call(self.client.call - ('AfterEffects.get_work_area', - item_id=item_id + ('AfterEffects.get_comp_properties', + item_id=comp_id )) records = self._to_records(self._handle_return(res)) if records: return records.pop() - def set_work_area(self, item, start, duration, frame_rate): + def set_comp_properties(self, comp_id, start, duration, frame_rate, + width, height): """ Set work area to predefined values (from Ftrack). Work area directs what gets rendered. Beware of rounding, AE expects seconds, not frames directly. Args: - item (dict): - start (float): workAreaStart in seconds - duration (float): in seconds + comp_id (int): + start (int): workAreaStart in frames + duration (int): in frames frame_rate (float): frames in seconds + width (int): resolution width + height (int): resolution height """ res = self.websocketserver.call(self.client.call - ('AfterEffects.set_work_area', - item_id=item.id, + ('AfterEffects.set_comp_properties', + item_id=comp_id, start=start, duration=duration, - frame_rate=frame_rate)) + frame_rate=frame_rate, + width=width, + height=height)) return self._handle_return(res) def save(self): @@ -554,6 +566,12 @@ class AfterEffectsServerStub(): return self._handle_return(res) + def print_msg(self, msg): + """Triggers Javascript alert dialog.""" + self.websocketserver.call(self.client.call + ('AfterEffects.print_msg', + msg=msg)) + def _handle_return(self, res): """Wraps return, throws ValueError if 'error' key is present.""" if res and isinstance(res, str) and res != "undefined": @@ -608,8 +626,8 @@ class AfterEffectsServerStub(): d.get('name'), d.get('type'), d.get('members'), - d.get('workAreaStart'), - d.get('workAreaDuration'), + d.get('frameStart'), + d.get('framesDuration'), d.get('frameRate'), d.get('file_name'), d.get("instance_id"), @@ -618,3 +636,18 @@ class AfterEffectsServerStub(): ret.append(item) return ret + + +def get_stub(): + """ + Convenience function to get server RPC stub to call methods directed + for host (Photoshop). + It expects already created connection, started from client. + Currently created when panel is opened (PS: Window>Extensions>Avalon) + :return: where functions could be called from + """ + ae_stub = AfterEffectsServerStub() + if not ae_stub.client: + raise ConnectionNotEstablishedYet("Connection is not created yet") + + return ae_stub diff --git a/openpype/hosts/aftereffects/plugins/create/create_render.py b/openpype/hosts/aftereffects/plugins/create/create_render.py index 171d7053ce..fa79fac78f 100644 --- a/openpype/hosts/aftereffects/plugins/create/create_render.py +++ b/openpype/hosts/aftereffects/plugins/create/create_render.py @@ -9,6 +9,7 @@ from openpype.pipeline import ( CreatorError ) from openpype.hosts.aftereffects.api.pipeline import cache_and_get_instances +from openpype.hosts.aftereffects.api.lib import set_settings from openpype.lib import prepare_template_data from openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS @@ -32,6 +33,14 @@ class RenderCreator(Creator): def create(self, subset_name_from_ui, data, pre_create_data): stub = api.get_stub() # only after After Effects is up + + try: + _ = stub.get_active_document_full_name() + except ValueError: + raise CreatorError( + "Please save workfile via Workfile app first!" + ) + if pre_create_data.get("use_selection"): comps = stub.get_selected_items( comps=True, folders=False, footages=False @@ -41,8 +50,8 @@ class RenderCreator(Creator): if not comps: raise CreatorError( - "Nothing to create. Select composition " - "if 'useSelection' or create at least " + "Nothing to create. Select composition in Project Bin if " + "'Use selection' is toggled or create at least " "one composition." ) use_composition_name = (pre_create_data.get("use_composition_name") or @@ -87,10 +96,14 @@ class RenderCreator(Creator): self._add_instance_to_context(new_instance) stub.rename_item(comp.id, subset_name) + set_settings(True, True, [comp.id], print_msg=False) def get_pre_create_attr_defs(self): output = [ - BoolDef("use_selection", default=True, label="Use selection"), + BoolDef("use_selection", + tooltip="Composition for publishable instance should be " + "selected by default.", + default=True, label="Use selection"), BoolDef("use_composition_name", label="Use composition name in subset"), UISeparatorDef(), diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_render.py b/openpype/hosts/aftereffects/plugins/publish/collect_render.py index b01b707246..aa46461915 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_render.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_render.py @@ -66,19 +66,19 @@ class CollectAERender(publish.AbstractCollectRender): comp_id = int(inst.data["members"][0]) - work_area_info = CollectAERender.get_stub().get_work_area(comp_id) + comp_info = CollectAERender.get_stub().get_comp_properties( + comp_id) - if not work_area_info: + if not comp_info: self.log.warning("Orphaned instance, deleting metadata") - inst_id = inst.get("instance_id") or str(comp_id) + inst_id = inst.data.get("instance_id") or str(comp_id) CollectAERender.get_stub().remove_instance(inst_id) continue - frame_start = work_area_info.workAreaStart - frame_end = round(work_area_info.workAreaStart + - float(work_area_info.workAreaDuration) * - float(work_area_info.frameRate)) - 1 - fps = work_area_info.frameRate + frame_start = comp_info.frameStart + frame_end = round(comp_info.frameStart + + comp_info.framesDuration) - 1 + fps = comp_info.frameRate # TODO add resolution when supported by extension task_name = inst.data.get("task") # legacy diff --git a/openpype/scripts/non_python_host_launch.py b/openpype/scripts/non_python_host_launch.py index 79fb1cbb52..c95a9df314 100644 --- a/openpype/scripts/non_python_host_launch.py +++ b/openpype/scripts/non_python_host_launch.py @@ -81,9 +81,10 @@ def main(argv): host_name = os.environ["AVALON_APP"].lower() if host_name == "photoshop": + # TODO refactor launch logic according to AE from openpype.hosts.photoshop.api.lib import main elif host_name == "aftereffects": - from openpype.hosts.aftereffects.api.lib import main + from openpype.hosts.aftereffects.api.launch_logic import main elif host_name == "harmony": from openpype.hosts.harmony.api.lib import main else: diff --git a/website/docs/artist_hosts_aftereffects.md b/website/docs/artist_hosts_aftereffects.md index d9522d5765..d415a1d47d 100644 --- a/website/docs/artist_hosts_aftereffects.md +++ b/website/docs/artist_hosts_aftereffects.md @@ -15,18 +15,18 @@ sidebar_label: AfterEffects ## Setup -To install the extension, download, install [Anastasyi's Extension Manager](https://install.anastasiy.com/). Open Anastasyi's Extension Manager and select AfterEffects in menu. Then go to `{path to pype}hosts/aftereffects/api/extension.zxp`. +To install the extension, download, install [Anastasyi's Extension Manager](https://install.anastasiy.com/). Open Anastasyi's Extension Manager and select AfterEffects in menu. Then go to `{path to pype}hosts/aftereffects/api/extension.zxp`. -Drag extension.zxp and drop it to Anastasyi's Extension Manager. The extension will install itself. +Drag extension.zxp and drop it to Anastasyi's Extension Manager. The extension will install itself. ## Implemented functionality AfterEffects implementation currently allows you to import and add various media to composition (image plates, renders, audio files, video files etc.) -and send prepared composition for rendering to Deadline or render locally. +and send prepared composition for rendering to Deadline or render locally. ## Usage -When you launch AfterEffects you will be met with the Workfiles app. If don't +When you launch AfterEffects you will be met with the Workfiles app. If don't have any previous workfiles, you can just close this window. Workfiles tools takes care of saving your .AEP files in the correct location and under @@ -34,7 +34,7 @@ a correct name. You should use it instead of standard file saving dialog. In AfterEffects you'll find the tools in the `OpenPype` extension: -![Extension](assets/photoshop_extension.png) +![Extension](assets/photoshop_extension.png) You can show the extension panel by going to `Window` > `Extensions` > `OpenPype`. @@ -58,6 +58,9 @@ Name of publishable instance (eg. subset name) could be configured with a templa Trash icon under the list of instances allows to delete any selected `render` instance. +Frame information (frame start, duration, fps) and resolution (width and height) is applied to selected composition from Asset Management System (Ftrack or DB) automatically! +(Eg. number of rendered frames is controlled by settings inserted from supervisor. Artist can override this by disabling validation only in special cases.) + Workfile instance will be automatically recreated though. If you do not want to publish it, use pill toggle on the instance item. If you would like to modify publishable instance, click on `Publish` tab at the top. This would allow you to change name of publishable @@ -67,7 +70,7 @@ Publisher allows publishing into different context, just click on any instance, #### RenderQueue -AE's Render Queue is required for publishing locally or on a farm. Artist needs to configure expected result format (extension, resolution) in the Render Queue in an Output module. +AE's Render Queue is required for publishing locally or on a farm. Artist needs to configure expected result format (extension, resolution) in the Render Queue in an Output module. Currently its expected to have only single render item per composition in the Render Queue. @@ -151,3 +154,25 @@ You can switch to a previous version of the image or update to the latest. ![Loader](assets/photoshop_manage_switch.gif) ![Loader](assets/photoshop_manage_update.gif) + + +### Setting section + +Composition properties should be controlled by state in Asset Management System (Ftrack etc). Extension provides couple of buttons to trigger this propagation. + +#### Set Resolution + +Set width and height from AMS to composition. + +#### Set Frame Range + +Start frame and duration in workarea is set according to the settings in AMS. Handles are incorporated (not inclusive). +It is expected that composition(s) is selected first before pushing this button! + +#### Apply All Settings + +Both previous settings are triggered at same time. + +### Experimental tools + +Currently empty. Could contain special tools available only for specific hosts for early access testing. diff --git a/website/docs/assets/aftereffects_extension.png b/website/docs/assets/aftereffects_extension.png new file mode 100644 index 0000000000000000000000000000000000000000..b14992471a1e42cb07c16e93858f180240f3f2e3 GIT binary patch literal 12533 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 Date: Mon, 22 May 2023 11:31:56 +0200 Subject: [PATCH 089/189] files are missing issue --- openpype/hosts/nuke/plugins/publish/collect_writes.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/collect_writes.py b/openpype/hosts/nuke/plugins/publish/collect_writes.py index 6697a1e59a..dbd0963f0a 100644 --- a/openpype/hosts/nuke/plugins/publish/collect_writes.py +++ b/openpype/hosts/nuke/plugins/publish/collect_writes.py @@ -133,11 +133,12 @@ class CollectNukeWrites(pyblish.api.InstancePlugin, else: representation['files'] = collected_frames - # inject colorspace data - self.set_representation_colorspace( - representation, instance.context, - colorspace=colorspace - ) + self.log.debug("_ representation: {}".format(representation)) + # inject colorspace data + self.set_representation_colorspace( + representation, instance.context, + colorspace=colorspace + ) instance.data["representations"].append(representation) self.log.info("Publishing rendered frames ...") From 30fe6759c32fb879fe9ddb3594aabe651c057697 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 22 May 2023 13:28:09 +0200 Subject: [PATCH 090/189] Publish: Enhance automated publish plugin settings (#4986) * prepared helper functions for custom settings apply method * publish plugin can have 'settings_category' attribute to define settings category * Better 'settings_category' comment Co-authored-by: Roy Nieterau * fix trailing spaces * added more information about pyblish plugins to dev docs --------- Co-authored-by: Roy Nieterau --- openpype/pipeline/publish/__init__.py | 6 ++ openpype/pipeline/publish/lib.py | 90 ++++++++++++++++++++------- website/docs/dev_publishing.md | 63 ++++++++++++++++++- 3 files changed, 136 insertions(+), 23 deletions(-) diff --git a/openpype/pipeline/publish/__init__.py b/openpype/pipeline/publish/__init__.py index 36252c9f3d..72f3774e1a 100644 --- a/openpype/pipeline/publish/__init__.py +++ b/openpype/pipeline/publish/__init__.py @@ -36,6 +36,9 @@ from .lib import ( context_plugin_should_run, get_instance_staging_dir, get_publish_repre_path, + + apply_plugin_settings_automatically, + get_plugin_settings, ) from .abstract_expected_files import ExpectedFiles @@ -80,6 +83,9 @@ __all__ = ( "get_instance_staging_dir", "get_publish_repre_path", + "apply_plugin_settings_automatically", + "get_plugin_settings", + "ExpectedFiles", "RenderInstance", diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index 8b6212b3ef..080f93e514 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -355,29 +355,55 @@ def publish_plugins_discover(paths=None): return result -def _get_plugin_settings(host_name, project_settings, plugin, log): +def get_plugin_settings(plugin, project_settings, log, category=None): """Get plugin settings based on host name and plugin name. + Note: + Default implementation of automated settings is passing host name + into 'category'. + Args: - host_name (str): Name of host. + plugin (pyblish.Plugin): Plugin where settings are applied. project_settings (dict[str, Any]): Project settings. - plugin (pyliblish.Plugin): Plugin where settings are applied. log (logging.Logger): Logger to log messages. + category (Optional[str]): Settings category key where to look + for plugin settings. Returns: dict[str, Any]: Plugin settings {'attribute': 'value'}. """ - # Use project settings from host name category when available - try: - return ( - project_settings - [host_name] - ["publish"] - [plugin.__name__] - ) - except KeyError: - pass + # Plugin can define settings category by class attribute + # - it's impossible to set `settings_category` via settings because + # obviously settings are not applied before it. + # - if `settings_category` is set the fallback category method is ignored + settings_category = getattr(plugin, "settings_category", None) + if settings_category: + try: + return ( + project_settings + [settings_category] + ["publish"] + [plugin.__name__] + ) + except KeyError: + log.warning(( + "Couldn't find plugin '{}' settings" + " under settings category '{}'" + ).format(plugin.__name__, settings_category)) + return {} + + # Use project settings based on a category name + if category: + try: + return ( + project_settings + [category] + ["publish"] + [plugin.__name__] + ) + except KeyError: + pass # Settings category determined from path # - usually path is './/plugins/publish/' @@ -386,9 +412,10 @@ def _get_plugin_settings(host_name, project_settings, plugin, log): split_path = filepath.rsplit(os.path.sep, 5) if len(split_path) < 4: - log.warning( - 'plugin path too short to extract host {}'.format(filepath) - ) + log.debug(( + "Plugin path is too short to automatically" + " extract settings category. {}" + ).format(filepath)) return {} category_from_file = split_path[-4] @@ -410,6 +437,28 @@ def _get_plugin_settings(host_name, project_settings, plugin, log): return {} +def apply_plugin_settings_automatically(plugin, settings, logger=None): + """Automatically apply plugin settings to a plugin object. + + Note: + This function was created to be able to use it in custom overrides of + 'apply_settings' class method. + + Args: + plugin (type[pyblish.api.Plugin]): Class of a plugin. + settings (dict[str, Any]): Plugin specific settings. + logger (Optional[logging.Logger]): Logger to log debug messages about + applied settings values. + """ + + for option, value in settings.items(): + if logger: + logger.debug("Plugin {} - Attr: {} -> {}".format( + option, value, plugin.__name__ + )) + setattr(plugin, option, value) + + def filter_pyblish_plugins(plugins): """Pyblish plugin filter which applies OpenPype settings. @@ -453,13 +502,10 @@ def filter_pyblish_plugins(plugins): ) else: # Automated - plugin_settins = _get_plugin_settings( - host_name, project_settings, plugin, log + plugin_settins = get_plugin_settings( + plugin, project_settings, log, host_name ) - for option, value in plugin_settins.items(): - log.info("setting {}:{} on plugin {}".format( - option, value, plugin.__name__)) - setattr(plugin, option, value) + apply_plugin_settings_automatically(plugin, plugin_settins, log) # Remove disabled plugins if getattr(plugin, "enabled", True) is False: diff --git a/website/docs/dev_publishing.md b/website/docs/dev_publishing.md index 2c57537223..3ef6272373 100644 --- a/website/docs/dev_publishing.md +++ b/website/docs/dev_publishing.md @@ -506,6 +506,67 @@ or the scene file was copy pasted from different context. #### *Known errors* When there is a known error that can't be fixed by the user (e.g. can't connect to deadline service, etc.) `KnownPublishError` should be raised. The only difference is that its message is shown in UI to the artist otherwise a neutral message without context is shown. +### Plugins +Plugin is a single processing unit that can work with publish context and instances. + +#### Plugin types +There are 2 types of plugins - `InstancePlugin` and `ContextPlugin`. Be aware that inheritance of plugin from `InstancePlugin` or `ContextPlugin` actually does not affect if plugin is instance or context plugin, that is affected by argument name in `process` method. + +```python +import pyblish.api + + +# Context plugin +class MyContextPlugin(pyblish.api.ContextPlugin): + def process(self, context): + ... + +# Instance plugin +class MyInstancePlugin(pyblish.api.InstancePlugin): + def process(self, instance): + ... + +# Still an instance plugin +class MyOtherInstancePlugin(pyblish.api.ContextPlugin): + def process(self, instance): + ... +``` + +#### Plugin filtering +By pyblish logic, plugins have predefined filtering class attributes `hosts`, `targets` and `families`. Filter by `hosts` and `targets` are filters that are applied for current publishing process. Both filters are registered in `pyblish` module, `hosts` filtering may not match OpenPype host name (e.g. farm publishing uses `shell` in pyblish). Filter `families` works only on instance plugins and is dynamic during publish process by changing families of an instance. + +All filters are list of a strings `families = ["image"]`. Empty list is invalid filter and plugin will be skipped, to allow plugin for all values use a start `families = ["*"]`. For more detailed filtering options check [pyblish documentation](https://api.pyblish.com/pluginsystem). + +Each plugin must have order, there are 4 order milestones - Collect, Validate, Extract, Integration. Any plugin below collection order won't be processed. for more details check [pyblish documentation](https://api.pyblish.com/ordering). + +#### Plugin settings +Pyblish plugins may have settings. There are 2 ways how settings are applied, first is automated, and it's logic is based on function `filter_pyblish_plugins` in `./openpype/pipeline/publish/lib.py`, second is explicit by implementing class method `apply_settings` on a plugin. + + +Automated logic is expecting specific structure of project settings `project_settings[{category}]["plugins"]["publish"][{plugin class name}]`. The category is a key in root of project settings. There are currently 3 ways how the category key is received. +1. Use `settings_category` class attribute value from plugin. If `settings_category` is not `None` there is not any fallback to other way. +2. Use currently registered pyblish host. This will be probably deprecated soon. +3. Use 3rd folder name from a plugin filepath. From path `./maya/plugins/publish/collect_render.py` is used `maya` as the key. + +For any other use-case is recommended to use explicit approach by implementing `apply_settings` method. Must use `@classmethod` decorator and expect arguments for project settings and system settings. We're planning to support single argument with only project settings. +```python +import pyblish.api + + +class MyPlugin(pyblish.api.InstancePlugin): + profiles = [] + + @classmethod + def apply_settings(cls, project_settings, system_settings): + cls.profiles = ( + project_settings + ["addon"] + ["plugins"] + ["publish"] + ["vfx_profiles"] + ) +``` + ### Plugin extension Publish plugins can be extended by additional logic when inheriting from `OpenPypePyblishPluginMixin` which can be used as mixin (additional inheritance of class). Publish plugins that inherit from this mixin can define attributes that will be shown in **CreatedInstance**. One of the most important usages is to be able turn on/off optional plugins. @@ -596,4 +657,4 @@ Publish attributes work the same way as create attributes but the source of attr ### Create dialog ![Publisher UI - Create dialog](assets/publisher_create_dialog.png) -Create dialog is used by artist to create new instances in a context. The context selection can be enabled/disabled by changing `create_allow_context_change` on [creator plugin](#creator). In the middle part the artist selects what will be created and what variant it is. On the right side is information about the selected creator and its pre-create attributes. There is also a question mark button which extends the window and displays more detailed information about the creator. \ No newline at end of file +Create dialog is used by artist to create new instances in a context. The context selection can be enabled/disabled by changing `create_allow_context_change` on [creator plugin](#creator). In the middle part the artist selects what will be created and what variant it is. On the right side is information about the selected creator and its pre-create attributes. There is also a question mark button which extends the window and displays more detailed information about the creator. From cfa64b58e84024d7ad3b7adef8e38dd4b5dfd493 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 22 May 2023 13:38:56 +0100 Subject: [PATCH 091/189] Fix the frame range when loading camera --- .../hosts/unreal/plugins/load/load_camera.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 2303ed1ffc..84d025b37e 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -286,6 +286,26 @@ class CameraLoader(plugin.Loader): self.fname ) + # Set range of all sections + # Changing the range of the section is not enough. We need to change + # the frame of all the keys in the section. + for possessable in cam_seq.get_possessables(): + for tracks in possessable.get_tracks(): + for section in tracks.get_sections(): + section.set_range( + data.get('clipIn'), + data.get('clipOut') + 1) + for channel in section.get_all_channels(): + for key in channel.get_keys(): + old_time = key.get_time().get_editor_property( + 'frame_number') + old_time_value = old_time.get_editor_property( + 'value') + new_time = old_time_value + ( + data.get('clipIn') - data.get('frameStart') + ) + key.set_time(unreal.FrameNumber(value = new_time)) + # Create Asset Container unreal_pipeline.create_container( container=container_name, path=asset_dir) From bf77a9e5b9cf1c0be9b2988941c3e06378698bf5 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 22 May 2023 13:43:27 +0100 Subject: [PATCH 092/189] Hound fixes --- openpype/hosts/unreal/plugins/load/load_camera.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 84d025b37e..1bd398349f 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -304,7 +304,7 @@ class CameraLoader(plugin.Loader): new_time = old_time_value + ( data.get('clipIn') - data.get('frameStart') ) - key.set_time(unreal.FrameNumber(value = new_time)) + key.set_time(unreal.FrameNumber(value=new_time)) # Create Asset Container unreal_pipeline.create_container( From 41fbe3031f67b9ed9141939c1e70a955b8240d4d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 22 May 2023 14:59:58 +0200 Subject: [PATCH 093/189] fusion: asset_db is collecting by default. --- .../fusion/plugins/publish/collect_instances.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_instances.py b/openpype/hosts/fusion/plugins/publish/collect_instances.py index 458f00c7ed..6016baa2a9 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_instances.py +++ b/openpype/hosts/fusion/plugins/publish/collect_instances.py @@ -25,15 +25,16 @@ class CollectInstanceData(pyblish.api.InstancePlugin): frame_range_source = creator_attributes.get("frame_range_source") instance.data["frame_range_source"] = frame_range_source - if frame_range_source == "asset_db": - # get asset frame ranges - start = context.data["frameStart"] - end = context.data["frameEnd"] - handle_start = context.data["handleStart"] - handle_end = context.data["handleEnd"] - start_with_handle = start - handle_start - end_with_handle = end + handle_end + # get asset frame ranges to all instances + # render family instances `asset_db` render target + start = context.data["frameStart"] + end = context.data["frameEnd"] + handle_start = context.data["handleStart"] + handle_end = context.data["handleEnd"] + start_with_handle = start - handle_start + end_with_handle = end + handle_end + # conditions for render family instances if frame_range_source == "render_range": # set comp render frame ranges start = context.data["renderFrameStart"] From 72fee37af6293b93605b323cfd191b46130c812b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 22 May 2023 16:42:12 +0200 Subject: [PATCH 094/189] Allow to open with djv by extension instead of representation name (#5004) --- openpype/plugins/load/open_djv.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/load/open_djv.py b/openpype/plugins/load/open_djv.py index bc5fd64b87..5bb7a6aaa5 100644 --- a/openpype/plugins/load/open_djv.py +++ b/openpype/plugins/load/open_djv.py @@ -19,7 +19,8 @@ class OpenInDJV(load.LoaderPlugin): djv_list = existing_djv_path() families = ["*"] if djv_list else [] - representations = [ + representations = ["*"] + extensions = [ "cin", "dpx", "avi", "dv", "gif", "flv", "mkv", "mov", "mpg", "mpeg", "mp4", "m4v", "mxf", "iff", "z", "ifl", "jpeg", "jpg", "jfif", "lut", "1dl", "exr", "pic", "png", "ppm", "pnm", "pgm", "pbm", "rla", "rpf", From bad6aa2d96f86dee6291ed664a4ccf447242d821 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 22 May 2023 16:50:38 +0200 Subject: [PATCH 095/189] DJV open action `extensions` as `set` (#5005) * Allow to open with djv by extension instead of representation name * Turn extensions into `set` like on base loader class --------- Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/plugins/load/open_djv.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/load/open_djv.py b/openpype/plugins/load/open_djv.py index 5bb7a6aaa5..9c36e7f405 100644 --- a/openpype/plugins/load/open_djv.py +++ b/openpype/plugins/load/open_djv.py @@ -20,12 +20,12 @@ class OpenInDJV(load.LoaderPlugin): djv_list = existing_djv_path() families = ["*"] if djv_list else [] representations = ["*"] - extensions = [ + extensions = { "cin", "dpx", "avi", "dv", "gif", "flv", "mkv", "mov", "mpg", "mpeg", "mp4", "m4v", "mxf", "iff", "z", "ifl", "jpeg", "jpg", "jfif", "lut", "1dl", "exr", "pic", "png", "ppm", "pnm", "pgm", "pbm", "rla", "rpf", "sgi", "rgba", "rgb", "bw", "tga", "tiff", "tif", "img", "h264", - ] + } label = "Open in DJV" order = -10 From 267bad5ba6bd0d0b7558d6d3a106fd7c2c106998 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 22 May 2023 16:59:12 +0200 Subject: [PATCH 096/189] adding multiple reposition nodes attribute to settings --- .../defaults/project_settings/nuke.json | 38 +++++++++++++++++-- .../schemas/schema_nuke_publish.json | 35 ++++++++++++++++- 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 85dee73176..f01bdf7d50 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -358,12 +358,12 @@ "optional": true, "active": true }, - "ValidateGizmo": { + "ValidateBackdrop": { "enabled": true, "optional": true, "active": true }, - "ValidateBackdrop": { + "ValidateGizmo": { "enabled": true, "optional": true, "active": true @@ -401,7 +401,39 @@ false ] ] - } + }, + "reposition_nodes": [ + { + "node_class": "Reformat", + "knobs": [ + { + "type": "text", + "name": "type", + "value": "to format" + }, + { + "type": "text", + "name": "format", + "value": "HD_1080" + }, + { + "type": "text", + "name": "filter", + "value": "Lanczos6" + }, + { + "type": "bool", + "name": "black_outside", + "value": true + }, + { + "type": "bool", + "name": "pbb", + "value": false + } + ] + } + ] }, "ExtractReviewData": { "enabled": false diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json index ce9fa04c6a..3019c9b1b5 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json @@ -158,10 +158,43 @@ "label": "Nodes", "collapsible": true, "children": [ + { + "type": "label", + "label": "Nodes attribute will be deprecated in future releases. Use reposition_nodes instead." + }, { "type": "raw-json", "key": "nodes", - "label": "Nodes" + "label": "Nodes [depricated]" + }, + { + "type": "label", + "label": "Reposition knobs supported only. You can add multiple reformat nodes
and set their knobs. Order of reformat nodes is important. First reformat node
will be applied first and last reformat node will be applied last." + }, + { + "key": "reposition_nodes", + "type": "list", + "label": "Reposition nodes", + "object_type": { + "type": "dict", + "children": [ + { + "key": "node_class", + "label": "Node class", + "type": "text" + }, + { + "type": "schema_template", + "name": "template_nuke_knob_inputs", + "template_data": [ + { + "label": "Node knobs", + "key": "knobs" + } + ] + } + ] + } } ] } From b62d066390861ac9aa13e10fb58f2e33d17bdb58 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 22 May 2023 17:30:09 +0200 Subject: [PATCH 097/189] adding multi repositional nodes support to thumbnail exporter --- .../nuke/plugins/publish/extract_thumbnail.py | 40 +++++++++++++------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index f391ca1e7c..2336487b37 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -5,6 +5,8 @@ import pyblish.api from openpype.pipeline import publish from openpype.hosts.nuke import api as napi +from openpype.hosts.nuke.api.lib import set_node_knobs_from_settings + if sys.version_info[0] >= 3: unicode = str @@ -28,7 +30,7 @@ class ExtractThumbnail(publish.Extractor): bake_viewer_process = True bake_viewer_input_process = True nodes = {} - + reposition_nodes = [] def process(self, instance): if instance.data.get("farm"): @@ -123,18 +125,32 @@ class ExtractThumbnail(publish.Extractor): temporary_nodes.append(rnode) previous_node = rnode - reformat_node = nuke.createNode("Reformat") - ref_node = self.nodes.get("Reformat", None) - if ref_node: - for k, v in ref_node: - self.log.debug("k, v: {0}:{1}".format(k, v)) - if isinstance(v, unicode): - v = str(v) - reformat_node[k].setValue(v) + if not self.reposition_nodes: + # [deprecated] create reformat node old way + reformat_node = nuke.createNode("Reformat") + ref_node = self.nodes.get("Reformat", None) + if ref_node: + for k, v in ref_node: + self.log.debug("k, v: {0}:{1}".format(k, v)) + if isinstance(v, unicode): + v = str(v) + reformat_node[k].setValue(v) - reformat_node.setInput(0, previous_node) - previous_node = reformat_node - temporary_nodes.append(reformat_node) + reformat_node.setInput(0, previous_node) + previous_node = reformat_node + temporary_nodes.append(reformat_node) + else: + # create reformat node new way + for repo_node in self.reposition_nodes: + node_class = repo_node["node_class"] + knobs = repo_node["knobs"] + node = nuke.createNode(node_class) + set_node_knobs_from_settings(node, knobs) + + # connect in order + node.setInput(0, previous_node) + previous_node = node + temporary_nodes.append(node) # only create colorspace baking if toggled on if bake_viewer_process: From fa74cae511070afb4edd87028ecbcffd5c7f6142 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 22 May 2023 16:55:39 +0100 Subject: [PATCH 098/189] Implemented creator, loader and extractor for Unreal Levels --- .../unreal/plugins/create/create_umap.py | 46 ++++++ .../hosts/unreal/plugins/load/load_umap.py | 140 ++++++++++++++++++ .../publish/collect_instance_members.py | 2 +- .../unreal/plugins/publish/extract_umap.py | 48 ++++++ 4 files changed, 235 insertions(+), 1 deletion(-) create mode 100644 openpype/hosts/unreal/plugins/create/create_umap.py create mode 100644 openpype/hosts/unreal/plugins/load/load_umap.py create mode 100644 openpype/hosts/unreal/plugins/publish/extract_umap.py diff --git a/openpype/hosts/unreal/plugins/create/create_umap.py b/openpype/hosts/unreal/plugins/create/create_umap.py new file mode 100644 index 0000000000..34aa8cdc00 --- /dev/null +++ b/openpype/hosts/unreal/plugins/create/create_umap.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +from pathlib import Path + +import unreal + +from openpype.pipeline import CreatorError +from openpype.hosts.unreal.api.plugin import ( + UnrealAssetCreator, +) + + +class CreateUMap(UnrealAssetCreator): + """Create Level.""" + + identifier = "io.ayon.creators.unreal.umap" + label = "Level" + family = "uasset" + icon = "cube" + + def create(self, subset_name, instance_data, pre_create_data): + if pre_create_data.get("use_selection"): + ar = unreal.AssetRegistryHelpers.get_asset_registry() + + sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() + selection = [a.get_path_name() for a in sel_objects] + + if len(selection) != 1: + raise CreatorError("Please select only one object.") + + obj = selection[0] + + asset = ar.get_asset_by_object_path(obj).get_asset() + sys_path = unreal.SystemLibrary.get_system_path(asset) + + if not sys_path: + raise CreatorError( + f"{Path(obj).name} is not on the disk. Likely it needs to" + "be saved first.") + + if Path(sys_path).suffix != ".umap": + raise CreatorError(f"{Path(sys_path).name} is not a Level.") + + super(CreateUMap, self).create( + subset_name, + instance_data, + pre_create_data) diff --git a/openpype/hosts/unreal/plugins/load/load_umap.py b/openpype/hosts/unreal/plugins/load/load_umap.py new file mode 100644 index 0000000000..f467fe6b3b --- /dev/null +++ b/openpype/hosts/unreal/plugins/load/load_umap.py @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- +"""Load Level.""" +from pathlib import Path +import shutil + +from openpype.pipeline import ( + get_representation_path, + AYON_CONTAINER_ID +) +from openpype.hosts.unreal.api import plugin +from openpype.hosts.unreal.api import pipeline as unreal_pipeline +import unreal # noqa + + +class UMapLoader(plugin.Loader): + """Load Level.""" + + families = ["uasset"] + label = "Load Level" + representations = ["umap"] + icon = "cube" + color = "orange" + + def load(self, context, name, namespace, options): + """Load and containerise representation into Content Browser. + + Args: + context (dict): application context + name (str): subset name + namespace (str): in Unreal this is basically path to container. + This is not passed here, so namespace is set + by `containerise()` because only then we know + real path. + options (dict): Those would be data to be imprinted. This is not + used now, data are imprinted by `containerise()`. + + Returns: + list(str): list of container content + """ + + # Create directory for asset and Ayon container + root = "/Game/Ayon/Assets" + asset = context.get('asset').get('name') + suffix = "_CON" + asset_name = f"{asset}_{name}" if asset else f"{name}" + tools = unreal.AssetToolsHelpers().get_asset_tools() + asset_dir, container_name = tools.create_unique_asset_name( + f"{root}/{asset}/{name}", suffix="" + ) + + container_name += suffix + + unreal.EditorAssetLibrary.make_directory(asset_dir) + + destination_path = asset_dir.replace( + "/Game", + Path(unreal.Paths.project_content_dir()).as_posix(), + 1) + + shutil.copy(self.fname, f"{destination_path}/{name}.uasset") + + # Create Asset Container + unreal_pipeline.create_container( + container=container_name, path=asset_dir) + + data = { + "schema": "ayon:container-2.0", + "id": AYON_CONTAINER_ID, + "asset": asset, + "namespace": asset_dir, + "container_name": container_name, + "asset_name": asset_name, + "loader": str(self.__class__.__name__), + "representation": context["representation"]["_id"], + "parent": context["representation"]["parent"], + "family": context["representation"]["context"]["family"] + } + unreal_pipeline.imprint(f"{asset_dir}/{container_name}", data) + + asset_content = unreal.EditorAssetLibrary.list_assets( + asset_dir, recursive=True, include_folder=True + ) + + for a in asset_content: + unreal.EditorAssetLibrary.save_asset(a) + + return asset_content + + def update(self, container, representation): + ar = unreal.AssetRegistryHelpers.get_asset_registry() + + asset_dir = container["namespace"] + name = representation["context"]["subset"] + + destination_path = asset_dir.replace( + "/Game", + Path(unreal.Paths.project_content_dir()).as_posix(), + 1) + + asset_content = unreal.EditorAssetLibrary.list_assets( + asset_dir, recursive=False, include_folder=True + ) + + for asset in asset_content: + obj = ar.get_asset_by_object_path(asset).get_asset() + if obj.get_class().get_name() != 'AyonAssetContainer': + unreal.EditorAssetLibrary.delete_asset(asset) + + update_filepath = get_representation_path(representation) + + shutil.copy(update_filepath, f"{destination_path}/{name}.umap") + + container_path = f'{container["namespace"]}/{container["objectName"]}' + # update metadata + unreal_pipeline.imprint( + container_path, + { + "representation": str(representation["_id"]), + "parent": str(representation["parent"]) + }) + + asset_content = unreal.EditorAssetLibrary.list_assets( + asset_dir, recursive=True, include_folder=True + ) + + for a in asset_content: + unreal.EditorAssetLibrary.save_asset(a) + + def remove(self, container): + path = container["namespace"] + parent_path = Path(path).parent.as_posix() + + unreal.EditorAssetLibrary.delete_directory(path) + + asset_content = unreal.EditorAssetLibrary.list_assets( + parent_path, recursive=False + ) + + if len(asset_content) == 0: + unreal.EditorAssetLibrary.delete_directory(parent_path) diff --git a/openpype/hosts/unreal/plugins/publish/collect_instance_members.py b/openpype/hosts/unreal/plugins/publish/collect_instance_members.py index 46ca51ab7e..de10e7b119 100644 --- a/openpype/hosts/unreal/plugins/publish/collect_instance_members.py +++ b/openpype/hosts/unreal/plugins/publish/collect_instance_members.py @@ -24,7 +24,7 @@ class CollectInstanceMembers(pyblish.api.InstancePlugin): ar = unreal.AssetRegistryHelpers.get_asset_registry() inst_path = instance.data.get('instance_path') - inst_name = instance.data.get('objectName') + inst_name = inst_path.split('/')[-1] pub_instance = ar.get_asset_by_object_path( f"{inst_path}.{inst_name}").get_asset() diff --git a/openpype/hosts/unreal/plugins/publish/extract_umap.py b/openpype/hosts/unreal/plugins/publish/extract_umap.py new file mode 100644 index 0000000000..3812834430 --- /dev/null +++ b/openpype/hosts/unreal/plugins/publish/extract_umap.py @@ -0,0 +1,48 @@ +from pathlib import Path +import shutil + +import unreal + +from openpype.pipeline import publish + + +class ExtractUMap(publish.Extractor): + """Extract a UMap.""" + + label = "Extract Level" + hosts = ["unreal"] + families = ["uasset"] + optional = True + + def process(self, instance): + ar = unreal.AssetRegistryHelpers.get_asset_registry() + + self.log.info("Performing extraction..") + + staging_dir = self.staging_dir(instance) + filename = f"{instance.name}.umap" + + members = instance.data.get("members", []) + + if not members: + raise RuntimeError("No members found in instance.") + + # UAsset publishing supports only one member + obj = members[0] + + asset = ar.get_asset_by_object_path(obj).get_asset() + sys_path = unreal.SystemLibrary.get_system_path(asset) + filename = Path(sys_path).name + + shutil.copy(sys_path, staging_dir) + + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': 'umap', + 'ext': 'umap', + 'files': filename, + "stagingDir": staging_dir, + } + instance.data["representations"].append(representation) From 14360f02176b3eb5fbb5a1bc9ba7ab54e33e6bd0 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 22 May 2023 18:11:13 +0100 Subject: [PATCH 099/189] Changed name and path of the camera levels to fix problem with sequencer --- openpype/hosts/unreal/api/__init__.py | 4 + openpype/hosts/unreal/api/pipeline.py | 130 ++++++++++++ .../hosts/unreal/plugins/load/load_camera.py | 192 +++++++----------- .../hosts/unreal/plugins/load/load_layout.py | 152 ++------------ 4 files changed, 228 insertions(+), 250 deletions(-) diff --git a/openpype/hosts/unreal/api/__init__.py b/openpype/hosts/unreal/api/__init__.py index de0fce13d5..ac6a91eae9 100644 --- a/openpype/hosts/unreal/api/__init__.py +++ b/openpype/hosts/unreal/api/__init__.py @@ -22,6 +22,8 @@ from .pipeline import ( show_tools_popup, instantiate, UnrealHost, + set_sequence_hierarchy, + generate_sequence, maintained_selection ) @@ -41,5 +43,7 @@ __all__ = [ "show_tools_popup", "instantiate", "UnrealHost", + "set_sequence_hierarchy", + "generate_sequence", "maintained_selection" ] diff --git a/openpype/hosts/unreal/api/pipeline.py b/openpype/hosts/unreal/api/pipeline.py index bb45fa8c01..5030e8ee86 100644 --- a/openpype/hosts/unreal/api/pipeline.py +++ b/openpype/hosts/unreal/api/pipeline.py @@ -9,12 +9,14 @@ import time import pyblish.api +from openpype.client import get_asset_by_name, get_assets from openpype.pipeline import ( register_loader_plugin_path, register_creator_plugin_path, deregister_loader_plugin_path, deregister_creator_plugin_path, AYON_CONTAINER_ID, + legacy_io, ) from openpype.tools.utils import host_tools import openpype.hosts.unreal @@ -512,6 +514,134 @@ def get_subsequences(sequence: unreal.LevelSequence): return [] +def set_sequence_hierarchy( + seq_i, seq_j, max_frame_i, min_frame_j, max_frame_j, map_paths +): + # Get existing sequencer tracks or create them if they don't exist + tracks = seq_i.get_master_tracks() + subscene_track = None + visibility_track = None + for t in tracks: + if t.get_class() == unreal.MovieSceneSubTrack.static_class(): + subscene_track = t + if (t.get_class() == + unreal.MovieSceneLevelVisibilityTrack.static_class()): + visibility_track = t + if not subscene_track: + subscene_track = seq_i.add_master_track(unreal.MovieSceneSubTrack) + if not visibility_track: + visibility_track = seq_i.add_master_track( + unreal.MovieSceneLevelVisibilityTrack) + + # Create the sub-scene section + subscenes = subscene_track.get_sections() + subscene = None + for s in subscenes: + if s.get_editor_property('sub_sequence') == seq_j: + subscene = s + break + if not subscene: + subscene = subscene_track.add_section() + subscene.set_row_index(len(subscene_track.get_sections())) + subscene.set_editor_property('sub_sequence', seq_j) + subscene.set_range( + min_frame_j, + max_frame_j + 1) + + # Create the visibility section + ar = unreal.AssetRegistryHelpers.get_asset_registry() + maps = [] + for m in map_paths: + # Unreal requires to load the level to get the map name + unreal.EditorLevelLibrary.save_all_dirty_levels() + unreal.EditorLevelLibrary.load_level(m) + maps.append(str(ar.get_asset_by_object_path(m).asset_name)) + + vis_section = visibility_track.add_section() + index = len(visibility_track.get_sections()) + + vis_section.set_range( + min_frame_j, + max_frame_j + 1) + vis_section.set_visibility(unreal.LevelVisibility.VISIBLE) + vis_section.set_row_index(index) + vis_section.set_level_names(maps) + + if min_frame_j > 1: + hid_section = visibility_track.add_section() + hid_section.set_range( + 1, + min_frame_j) + hid_section.set_visibility(unreal.LevelVisibility.HIDDEN) + hid_section.set_row_index(index) + hid_section.set_level_names(maps) + if max_frame_j < max_frame_i: + hid_section = visibility_track.add_section() + hid_section.set_range( + max_frame_j + 1, + max_frame_i + 1) + hid_section.set_visibility(unreal.LevelVisibility.HIDDEN) + hid_section.set_row_index(index) + hid_section.set_level_names(maps) + + +def generate_sequence(h, h_dir): + tools = unreal.AssetToolsHelpers().get_asset_tools() + + sequence = tools.create_asset( + asset_name=h, + package_path=h_dir, + asset_class=unreal.LevelSequence, + factory=unreal.LevelSequenceFactoryNew() + ) + + project_name = legacy_io.active_project() + asset_data = get_asset_by_name( + project_name, + h_dir.split('/')[-1], + fields=["_id", "data.fps"] + ) + + start_frames = [] + end_frames = [] + + elements = list(get_assets( + project_name, + parent_ids=[asset_data["_id"]], + fields=["_id", "data.clipIn", "data.clipOut"] + )) + for e in elements: + start_frames.append(e.get('data').get('clipIn')) + end_frames.append(e.get('data').get('clipOut')) + + elements.extend(get_assets( + project_name, + parent_ids=[e["_id"]], + fields=["_id", "data.clipIn", "data.clipOut"] + )) + + min_frame = min(start_frames) + max_frame = max(end_frames) + + sequence.set_display_rate( + unreal.FrameRate(asset_data.get('data').get("fps"), 1.0)) + sequence.set_playback_start(min_frame) + sequence.set_playback_end(max_frame) + + tracks = sequence.get_master_tracks() + track = None + for t in tracks: + if (t.get_class() == + unreal.MovieSceneCameraCutTrack.static_class()): + track = t + break + if not track: + track = sequence.add_master_track( + unreal.MovieSceneCameraCutTrack) + + return sequence, (min_frame, max_frame) + + @contextmanager def maintained_selection(): """Stub to be either implemented or replaced. diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 1bd398349f..02634104e7 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -6,13 +6,18 @@ import unreal from unreal import EditorAssetLibrary from unreal import EditorLevelLibrary from unreal import EditorLevelUtils -from openpype.client import get_assets, get_asset_by_name +from openpype.client import get_asset_by_name from openpype.pipeline import ( AYON_CONTAINER_ID, legacy_io, ) from openpype.hosts.unreal.api import plugin -from openpype.hosts.unreal.api import pipeline as unreal_pipeline +from openpype.hosts.unreal.api.pipeline import ( + generate_sequence, + set_sequence_hierarchy, + create_container, + imprint, +) class CameraLoader(plugin.Loader): @@ -24,32 +29,6 @@ class CameraLoader(plugin.Loader): icon = "cube" color = "orange" - def _set_sequence_hierarchy( - self, seq_i, seq_j, min_frame_j, max_frame_j - ): - tracks = seq_i.get_master_tracks() - track = None - for t in tracks: - if t.get_class() == unreal.MovieSceneSubTrack.static_class(): - track = t - break - if not track: - track = seq_i.add_master_track(unreal.MovieSceneSubTrack) - - subscenes = track.get_sections() - subscene = None - for s in subscenes: - if s.get_editor_property('sub_sequence') == seq_j: - subscene = s - break - if not subscene: - subscene = track.add_section() - subscene.set_row_index(len(track.get_sections())) - subscene.set_editor_property('sub_sequence', seq_j) - subscene.set_range( - min_frame_j, - max_frame_j + 1) - def _import_camera( self, world, sequence, bindings, import_fbx_settings, import_filename ): @@ -156,9 +135,9 @@ class CameraLoader(plugin.Loader): if not EditorAssetLibrary.does_asset_exist(master_level): EditorLevelLibrary.new_level(f"{h_dir}/{h_asset}_map") - level = f"{asset_path_parent}/{asset}_map.{asset}_map" + level = f"{asset_dir}/{asset}_map_camera.{asset}_map_camera" if not EditorAssetLibrary.does_asset_exist(level): - EditorLevelLibrary.new_level(f"{asset_path_parent}/{asset}_map") + EditorLevelLibrary.new_level(f"{asset_dir}/{asset}_map_camera") EditorLevelLibrary.load_level(master_level) EditorLevelUtils.add_level_to_world( @@ -169,27 +148,13 @@ class CameraLoader(plugin.Loader): EditorLevelLibrary.save_all_dirty_levels() EditorLevelLibrary.load_level(level) - project_name = legacy_io.active_project() - # TODO refactor - # - Creating of hierarchy should be a function in unreal integration - # - it's used in multiple loaders but must not be loader's logic - # - hard to say what is purpose of the loop - # - variables does not match their meaning - # - why scene is stored to sequences? - # - asset documents vs. elements - # - cleanup variable names in whole function - # - e.g. 'asset', 'asset_name', 'asset_data', 'asset_doc' - # - really inefficient queries of asset documents - # - existing asset in scene is considered as "with correct values" - # - variable 'elements' is modified during it's loop # Get all the sequences in the hierarchy. It will create them, if # they don't exist. - sequences = [] frame_ranges = [] - i = 0 - for h in hierarchy_dir_list: + sequences = [] + for (h_dir, h) in zip(hierarchy_dir_list, hierarchy): root_content = EditorAssetLibrary.list_assets( - h, recursive=False, include_folder=False) + h_dir, recursive=False, include_folder=False) existing_sequences = [ EditorAssetLibrary.find_asset_data(asset) @@ -199,48 +164,10 @@ class CameraLoader(plugin.Loader): ] if not existing_sequences: - scene = tools.create_asset( - asset_name=hierarchy[i], - package_path=h, - asset_class=unreal.LevelSequence, - factory=unreal.LevelSequenceFactoryNew() - ) + sequence, frame_range = generate_sequence(h, h_dir) - asset_data = get_asset_by_name( - project_name, - h.split('/')[-1], - fields=["_id", "data.fps"] - ) - - start_frames = [] - end_frames = [] - - elements = list(get_assets( - project_name, - parent_ids=[asset_data["_id"]], - fields=["_id", "data.clipIn", "data.clipOut"] - )) - - for e in elements: - start_frames.append(e.get('data').get('clipIn')) - end_frames.append(e.get('data').get('clipOut')) - - elements.extend(get_assets( - project_name, - parent_ids=[e["_id"]], - fields=["_id", "data.clipIn", "data.clipOut"] - )) - - min_frame = min(start_frames) - max_frame = max(end_frames) - - scene.set_display_rate( - unreal.FrameRate(asset_data.get('data').get("fps"), 1.0)) - scene.set_playback_start(min_frame) - scene.set_playback_end(max_frame) - - sequences.append(scene) - frame_ranges.append((min_frame, max_frame)) + sequences.append(sequence) + frame_ranges.append(frame_range) else: for e in existing_sequences: sequences.append(e.get_asset()) @@ -248,8 +175,6 @@ class CameraLoader(plugin.Loader): e.get_asset().get_playback_start(), e.get_asset().get_playback_end())) - i += 1 - EditorAssetLibrary.make_directory(asset_dir) cam_seq = tools.create_asset( @@ -260,19 +185,24 @@ class CameraLoader(plugin.Loader): ) # Add sequences data to hierarchy - for i in range(0, len(sequences) - 1): - self._set_sequence_hierarchy( + for i in range(len(sequences) - 1): + set_sequence_hierarchy( sequences[i], sequences[i + 1], - frame_ranges[i + 1][0], frame_ranges[i + 1][1]) + frame_ranges[i][1], + frame_ranges[i + 1][0], frame_ranges[i + 1][1], + [level]) + project_name = legacy_io.active_project() data = get_asset_by_name(project_name, asset)["data"] cam_seq.set_display_rate( unreal.FrameRate(data.get("fps"), 1.0)) cam_seq.set_playback_start(data.get('clipIn')) cam_seq.set_playback_end(data.get('clipOut') + 1) - self._set_sequence_hierarchy( + set_sequence_hierarchy( sequences[-1], cam_seq, - data.get('clipIn'), data.get('clipOut')) + frame_ranges[-1][1], + data.get('clipIn'), data.get('clipOut'), + [level]) settings = unreal.MovieSceneUserImportFBXSettings() settings.set_editor_property('reduce_keys', False) @@ -307,7 +237,7 @@ class CameraLoader(plugin.Loader): key.set_time(unreal.FrameNumber(value=new_time)) # Create Asset Container - unreal_pipeline.create_container( + create_container( container=container_name, path=asset_dir) data = { @@ -322,7 +252,7 @@ class CameraLoader(plugin.Loader): "parent": context["representation"]["parent"], "family": context["representation"]["context"]["family"] } - unreal_pipeline.imprint( + imprint( "{}/{}".format(asset_dir, container_name), data) EditorLevelLibrary.save_all_dirty_levels() @@ -360,7 +290,7 @@ class CameraLoader(plugin.Loader): sequences = ar.get_assets(filter) filter = unreal.ARFilter( class_names=["World"], - package_paths=[str(Path(asset_dir).parent.as_posix())], + package_paths=[asset_dir], recursive_paths=True) maps = ar.get_assets(filter) @@ -470,7 +400,7 @@ class CameraLoader(plugin.Loader): "representation": str(representation["_id"]), "parent": str(representation["parent"]) } - unreal_pipeline.imprint( + imprint( "{}/{}".format(asset_dir, container.get('container_name')), data) EditorLevelLibrary.save_current_level() @@ -484,15 +414,15 @@ class CameraLoader(plugin.Loader): EditorLevelLibrary.load_level(master_level) def remove(self, container): - path = Path(container.get("namespace")) - parent_path = str(path.parent.as_posix()) + asset_dir = container.get('namespace') + path = Path(asset_dir) ar = unreal.AssetRegistryHelpers.get_asset_registry() - filter = unreal.ARFilter( + _filter = unreal.ARFilter( class_names=["LevelSequence"], - package_paths=[f"{str(path.as_posix())}"], + package_paths=[asset_dir], recursive_paths=False) - sequences = ar.get_assets(filter) + sequences = ar.get_assets(_filter) if not sequences: raise Exception("Could not find sequence.") @@ -500,11 +430,11 @@ class CameraLoader(plugin.Loader): world = ar.get_asset_by_object_path( EditorLevelLibrary.get_editor_world().get_path_name()) - filter = unreal.ARFilter( + _filter = unreal.ARFilter( class_names=["World"], - package_paths=[f"{parent_path}"], + package_paths=[asset_dir], recursive_paths=True) - maps = ar.get_assets(filter) + maps = ar.get_assets(_filter) # There should be only one map in the list if not maps: @@ -534,12 +464,18 @@ class CameraLoader(plugin.Loader): root = "/Game/Ayon" namespace = container.get('namespace').replace(f"{root}/", "") ms_asset = namespace.split('/')[0] - filter = unreal.ARFilter( + _filter = unreal.ARFilter( class_names=["LevelSequence"], package_paths=[f"{root}/{ms_asset}"], recursive_paths=False) - sequences = ar.get_assets(filter) + sequences = ar.get_assets(_filter) master_sequence = sequences[0].get_asset() + _filter = unreal.ARFilter( + class_names=["World"], + package_paths=[f"{root}/{ms_asset}"], + recursive_paths=False) + levels = ar.get_assets(_filter) + master_level = levels[0].get_full_name() sequences = [master_sequence] @@ -547,10 +483,13 @@ class CameraLoader(plugin.Loader): for s in sequences: tracks = s.get_master_tracks() subscene_track = None + visibility_track = None for t in tracks: if t.get_class() == unreal.MovieSceneSubTrack.static_class(): subscene_track = t - break + if (t.get_class() == + unreal.MovieSceneLevelVisibilityTrack.static_class()): + visibility_track = t if subscene_track: sections = subscene_track.get_sections() for ss in sections: @@ -565,18 +504,45 @@ class CameraLoader(plugin.Loader): ss.set_row_index(i) i += 1 + if visibility_track: + sections = visibility_track.get_sections() + for ss in sections: + if (unreal.Name(f"{container.get('asset')}_map_camera") + in ss.get_level_names()): + visibility_track.remove_section(ss) + # Update visibility sections indexes. + i = -1 + prev_name = [] + for ss in sections: + if prev_name != ss.get_level_names(): + i += 1 + ss.set_row_index(i) + prev_name = ss.get_level_names() if parent: break assert parent, "Could not find the parent sequence" - EditorAssetLibrary.delete_directory(str(path.as_posix())) + # Create a temporary level to delete the layout level. + EditorLevelLibrary.save_all_dirty_levels() + EditorAssetLibrary.make_directory(f"{root}/tmp") + tmp_level = f"{root}/tmp/temp_map" + if not EditorAssetLibrary.does_asset_exist(f"{tmp_level}.temp_map"): + EditorLevelLibrary.new_level(tmp_level) + else: + EditorLevelLibrary.load_level(tmp_level) + + # Delete the layout directory. + EditorAssetLibrary.delete_directory(asset_dir) + + EditorLevelLibrary.load_level(master_level) + EditorAssetLibrary.delete_directory(f"{root}/tmp") # Check if there isn't any more assets in the parent folder, and # delete it if not. asset_content = EditorAssetLibrary.list_assets( - parent_path, recursive=False, include_folder=True + path.parent.as_posix(), recursive=False, include_folder=True ) if len(asset_content) == 0: - EditorAssetLibrary.delete_directory(parent_path) + EditorAssetLibrary.delete_directory(path.parent.as_posix()) diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index e5f32c3412..1dfe6f1f5c 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -13,7 +13,7 @@ from unreal import FBXImportType from unreal import MovieSceneLevelVisibilityTrack from unreal import MovieSceneSubTrack -from openpype.client import get_asset_by_name, get_assets, get_representations +from openpype.client import get_asset_by_name, get_representations from openpype.pipeline import ( discover_loader_plugins, loaders_from_representation, @@ -25,7 +25,13 @@ from openpype.pipeline import ( from openpype.pipeline.context_tools import get_current_project_asset from openpype.settings import get_current_project_settings from openpype.hosts.unreal.api import plugin -from openpype.hosts.unreal.api import pipeline as unreal_pipeline +from openpype.hosts.unreal.api.pipeline import ( + generate_sequence, + set_sequence_hierarchy, + create_container, + imprint, + ls, +) class LayoutLoader(plugin.Loader): @@ -91,77 +97,6 @@ class LayoutLoader(plugin.Loader): return None - @staticmethod - def _set_sequence_hierarchy( - seq_i, seq_j, max_frame_i, min_frame_j, max_frame_j, map_paths - ): - # Get existing sequencer tracks or create them if they don't exist - tracks = seq_i.get_master_tracks() - subscene_track = None - visibility_track = None - for t in tracks: - if t.get_class() == unreal.MovieSceneSubTrack.static_class(): - subscene_track = t - if (t.get_class() == - unreal.MovieSceneLevelVisibilityTrack.static_class()): - visibility_track = t - if not subscene_track: - subscene_track = seq_i.add_master_track(unreal.MovieSceneSubTrack) - if not visibility_track: - visibility_track = seq_i.add_master_track( - unreal.MovieSceneLevelVisibilityTrack) - - # Create the sub-scene section - subscenes = subscene_track.get_sections() - subscene = None - for s in subscenes: - if s.get_editor_property('sub_sequence') == seq_j: - subscene = s - break - if not subscene: - subscene = subscene_track.add_section() - subscene.set_row_index(len(subscene_track.get_sections())) - subscene.set_editor_property('sub_sequence', seq_j) - subscene.set_range( - min_frame_j, - max_frame_j + 1) - - # Create the visibility section - ar = unreal.AssetRegistryHelpers.get_asset_registry() - maps = [] - for m in map_paths: - # Unreal requires to load the level to get the map name - EditorLevelLibrary.save_all_dirty_levels() - EditorLevelLibrary.load_level(m) - maps.append(str(ar.get_asset_by_object_path(m).asset_name)) - - vis_section = visibility_track.add_section() - index = len(visibility_track.get_sections()) - - vis_section.set_range( - min_frame_j, - max_frame_j + 1) - vis_section.set_visibility(unreal.LevelVisibility.VISIBLE) - vis_section.set_row_index(index) - vis_section.set_level_names(maps) - - if min_frame_j > 1: - hid_section = visibility_track.add_section() - hid_section.set_range( - 1, - min_frame_j) - hid_section.set_visibility(unreal.LevelVisibility.HIDDEN) - hid_section.set_row_index(index) - hid_section.set_level_names(maps) - if max_frame_j < max_frame_i: - hid_section = visibility_track.add_section() - hid_section.set_range( - max_frame_j + 1, - max_frame_i + 1) - hid_section.set_visibility(unreal.LevelVisibility.HIDDEN) - hid_section.set_row_index(index) - hid_section.set_level_names(maps) - def _transform_from_basis(self, transform, basis): """Transform a transform from a basis to a new basis.""" # Get the basis matrix @@ -352,63 +287,6 @@ class LayoutLoader(plugin.Loader): sec_params = section.get_editor_property('params') sec_params.set_editor_property('animation', animation) - @staticmethod - def _generate_sequence(h, h_dir): - tools = unreal.AssetToolsHelpers().get_asset_tools() - - sequence = tools.create_asset( - asset_name=h, - package_path=h_dir, - asset_class=unreal.LevelSequence, - factory=unreal.LevelSequenceFactoryNew() - ) - - project_name = legacy_io.active_project() - asset_data = get_asset_by_name( - project_name, - h_dir.split('/')[-1], - fields=["_id", "data.fps"] - ) - - start_frames = [] - end_frames = [] - - elements = list(get_assets( - project_name, - parent_ids=[asset_data["_id"]], - fields=["_id", "data.clipIn", "data.clipOut"] - )) - for e in elements: - start_frames.append(e.get('data').get('clipIn')) - end_frames.append(e.get('data').get('clipOut')) - - elements.extend(get_assets( - project_name, - parent_ids=[e["_id"]], - fields=["_id", "data.clipIn", "data.clipOut"] - )) - - min_frame = min(start_frames) - max_frame = max(end_frames) - - sequence.set_display_rate( - unreal.FrameRate(asset_data.get('data').get("fps"), 1.0)) - sequence.set_playback_start(min_frame) - sequence.set_playback_end(max_frame) - - tracks = sequence.get_master_tracks() - track = None - for t in tracks: - if (t.get_class() == - unreal.MovieSceneCameraCutTrack.static_class()): - track = t - break - if not track: - track = sequence.add_master_track( - unreal.MovieSceneCameraCutTrack) - - return sequence, (min_frame, max_frame) - def _get_repre_docs_by_version_id(self, data): version_ids = { element.get("version") @@ -696,7 +574,7 @@ class LayoutLoader(plugin.Loader): ] if not existing_sequences: - sequence, frame_range = self._generate_sequence(h, h_dir) + sequence, frame_range = generate_sequence(h, h_dir) sequences.append(sequence) frame_ranges.append(frame_range) @@ -716,7 +594,7 @@ class LayoutLoader(plugin.Loader): # sequences and frame_ranges have the same length for i in range(0, len(sequences) - 1): - self._set_sequence_hierarchy( + set_sequence_hierarchy( sequences[i], sequences[i + 1], frame_ranges[i][1], frame_ranges[i + 1][0], frame_ranges[i + 1][1], @@ -729,7 +607,7 @@ class LayoutLoader(plugin.Loader): shot.set_playback_start(0) shot.set_playback_end(data.get('clipOut') - data.get('clipIn') + 1) if sequences: - self._set_sequence_hierarchy( + set_sequence_hierarchy( sequences[-1], shot, frame_ranges[-1][1], data.get('clipIn'), data.get('clipOut'), @@ -745,7 +623,7 @@ class LayoutLoader(plugin.Loader): EditorLevelLibrary.save_current_level() # Create Asset Container - unreal_pipeline.create_container( + create_container( container=container_name, path=asset_dir) data = { @@ -761,7 +639,7 @@ class LayoutLoader(plugin.Loader): "family": context["representation"]["context"]["family"], "loaded_assets": loaded_assets } - unreal_pipeline.imprint( + imprint( "{}/{}".format(asset_dir, container_name), data) asset_content = EditorAssetLibrary.list_assets( @@ -843,7 +721,7 @@ class LayoutLoader(plugin.Loader): "parent": str(representation["parent"]), "loaded_assets": loaded_assets } - unreal_pipeline.imprint( + imprint( "{}/{}".format(asset_dir, container.get('container_name')), data) EditorLevelLibrary.save_current_level() @@ -870,7 +748,7 @@ class LayoutLoader(plugin.Loader): root = "/Game/Ayon" path = Path(container.get("namespace")) - containers = unreal_pipeline.ls() + containers = ls() layout_containers = [ c for c in containers if (c.get('asset_name') != container.get('asset_name') and From 4adf8388b4c08738c99b3f4fd8dfb778fb841dee Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 23 May 2023 09:17:54 +0100 Subject: [PATCH 100/189] Fix problem with updating camera --- .../hosts/unreal/plugins/load/load_camera.py | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 02634104e7..9361ed2ac5 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -345,17 +345,21 @@ class CameraLoader(plugin.Loader): for s in sequences: tracks = s.get_master_tracks() subscene_track = None + visibility_track = None for t in tracks: if t.get_class() == unreal.MovieSceneSubTrack.static_class(): subscene_track = t - break + if (t.get_class() == + unreal.MovieSceneLevelVisibilityTrack.static_class()): + visibility_track = t if subscene_track: sections = subscene_track.get_sections() for ss in sections: - if ss.get_sequence().get_name() == sequence_name: + if (ss.get_sequence().get_name() == + container.get('asset')): parent = s sub_scene = ss - # subscene_track.remove_section(ss) + subscene_track.remove_section(ss) break sequences.append(ss.get_sequence()) # Update subscenes indexes. @@ -364,10 +368,24 @@ class CameraLoader(plugin.Loader): ss.set_row_index(i) i += 1 + if visibility_track: + sections = visibility_track.get_sections() + for ss in sections: + if (unreal.Name(f"{container.get('asset')}_map") + in ss.get_level_names()): + visibility_track.remove_section(ss) + # Update visibility sections indexes. + i = -1 + prev_name = [] + for ss in sections: + if prev_name != ss.get_level_names(): + i += 1 + ss.set_row_index(i) + prev_name = ss.get_level_names() if parent: break - assert parent, "Could not find the parent sequence" + assert parent, "Could not find the parent sequence" EditorAssetLibrary.delete_asset(level_sequence.get_path_name()) From 341dc16701a147807388df3467ce6c84094f7c40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Tue, 23 May 2023 11:22:06 +0200 Subject: [PATCH 101/189] Update openpype/hosts/nuke/plugins/publish/collect_writes.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/nuke/plugins/publish/collect_writes.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/collect_writes.py b/openpype/hosts/nuke/plugins/publish/collect_writes.py index dbd0963f0a..2d1caacdc3 100644 --- a/openpype/hosts/nuke/plugins/publish/collect_writes.py +++ b/openpype/hosts/nuke/plugins/publish/collect_writes.py @@ -133,7 +133,6 @@ class CollectNukeWrites(pyblish.api.InstancePlugin, else: representation['files'] = collected_frames - self.log.debug("_ representation: {}".format(representation)) # inject colorspace data self.set_representation_colorspace( representation, instance.context, From b7556f76d5cbc61841ec4593c99fa20dd42d7023 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 23 May 2023 10:23:14 +0100 Subject: [PATCH 102/189] Fix sequence when updating camera... again --- .../hosts/unreal/plugins/load/load_camera.py | 43 ++++--------------- 1 file changed, 9 insertions(+), 34 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 9361ed2ac5..6f20a466a9 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -270,7 +270,7 @@ class CameraLoader(plugin.Loader): def update(self, container, representation): ar = unreal.AssetRegistryHelpers.get_asset_registry() - root = "/Game/ayon" + root = "/Game/Ayon" asset_dir = container.get('namespace') @@ -283,16 +283,16 @@ class CameraLoader(plugin.Loader): EditorLevelLibrary.save_current_level() - filter = unreal.ARFilter( + _filter = unreal.ARFilter( class_names=["LevelSequence"], package_paths=[asset_dir], recursive_paths=False) - sequences = ar.get_assets(filter) - filter = unreal.ARFilter( + sequences = ar.get_assets(_filter) + _filter = unreal.ARFilter( class_names=["World"], package_paths=[asset_dir], recursive_paths=True) - maps = ar.get_assets(filter) + maps = ar.get_assets(_filter) # There should be only one map in the list EditorLevelLibrary.load_level(maps[0].get_full_name()) @@ -328,14 +328,13 @@ class CameraLoader(plugin.Loader): # Remove the Level Sequence from the parent. # We need to traverse the hierarchy from the master sequence to find # the level sequence. - root = "/Game/Ayon" namespace = container.get('namespace').replace(f"{root}/", "") ms_asset = namespace.split('/')[0] - filter = unreal.ARFilter( + _filter = unreal.ARFilter( class_names=["LevelSequence"], package_paths=[f"{root}/{ms_asset}"], recursive_paths=False) - sequences = ar.get_assets(filter) + sequences = ar.get_assets(_filter) master_sequence = sequences[0].get_asset() sequences = [master_sequence] @@ -345,43 +344,19 @@ class CameraLoader(plugin.Loader): for s in sequences: tracks = s.get_master_tracks() subscene_track = None - visibility_track = None for t in tracks: if t.get_class() == unreal.MovieSceneSubTrack.static_class(): subscene_track = t - if (t.get_class() == - unreal.MovieSceneLevelVisibilityTrack.static_class()): - visibility_track = t if subscene_track: sections = subscene_track.get_sections() for ss in sections: - if (ss.get_sequence().get_name() == - container.get('asset')): + if ss.get_sequence().get_name() == sequence_name: parent = s sub_scene = ss - subscene_track.remove_section(ss) break sequences.append(ss.get_sequence()) - # Update subscenes indexes. - i = 0 - for ss in sections: + for i, ss in enumerate(sections): ss.set_row_index(i) - i += 1 - - if visibility_track: - sections = visibility_track.get_sections() - for ss in sections: - if (unreal.Name(f"{container.get('asset')}_map") - in ss.get_level_names()): - visibility_track.remove_section(ss) - # Update visibility sections indexes. - i = -1 - prev_name = [] - for ss in sections: - if prev_name != ss.get_level_names(): - i += 1 - ss.set_row_index(i) - prev_name = ss.get_level_names() if parent: break From bf816562f09dc53510a8834e8e3c5ae9ba8b6597 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 23 May 2023 10:48:17 +0100 Subject: [PATCH 103/189] Fix old camera not being deleted on update --- .../hosts/unreal/plugins/load/load_camera.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 6f20a466a9..8fd6cc3e80 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -270,17 +270,8 @@ class CameraLoader(plugin.Loader): def update(self, container, representation): ar = unreal.AssetRegistryHelpers.get_asset_registry() - root = "/Game/Ayon" - asset_dir = container.get('namespace') - context = representation.get("context") - - hierarchy = context.get('hierarchy').split("/") - h_dir = f"{root}/{hierarchy[0]}" - h_asset = hierarchy[0] - master_level = f"{h_dir}/{h_asset}_map.{h_asset}_map" - EditorLevelLibrary.save_current_level() _filter = unreal.ARFilter( @@ -295,7 +286,7 @@ class CameraLoader(plugin.Loader): maps = ar.get_assets(_filter) # There should be only one map in the list - EditorLevelLibrary.load_level(maps[0].get_full_name()) + EditorLevelLibrary.load_level(maps[0].get_asset().get_path_name()) level_sequence = sequences[0].get_asset() @@ -328,6 +319,7 @@ class CameraLoader(plugin.Loader): # Remove the Level Sequence from the parent. # We need to traverse the hierarchy from the master sequence to find # the level sequence. + root = "/Game/Ayon" namespace = container.get('namespace').replace(f"{root}/", "") ms_asset = namespace.split('/')[0] _filter = unreal.ARFilter( @@ -336,6 +328,12 @@ class CameraLoader(plugin.Loader): recursive_paths=False) sequences = ar.get_assets(_filter) master_sequence = sequences[0].get_asset() + _filter = unreal.ARFilter( + class_names=["World"], + package_paths=[f"{root}/{ms_asset}"], + recursive_paths=False) + levels = ar.get_assets(_filter) + master_level = levels[0].get_asset().get_path_name() sequences = [master_sequence] From a762b310e8407a233185b2de18de09c4b0f44a27 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 23 May 2023 11:51:23 +0200 Subject: [PATCH 104/189] inverting logic for `ignoreFrameHandleCheck` this was ignoring settings in frame range target. --- openpype/hosts/fusion/plugins/publish/collect_render.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py index d0b7f1c4ff..551a365099 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_render.py +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -84,7 +84,7 @@ class CollectFusionRender( handleStart=inst.data["handleStart"], handleEnd=inst.data["handleEnd"], ignoreFrameHandleCheck=( - inst.data["frame_range_source"] == "render_range"), + inst.data["frame_range_source"] == "asset_db"), frameStep=1, fps=comp_frame_format_prefs.get("Rate"), app_version=comp.GetApp().Version, From a68aa029e4246aa77dcb67b003b3da7e7b650430 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 23 May 2023 11:52:09 +0200 Subject: [PATCH 105/189] Renaming attribute to make more sense in Fusion context --- openpype/hosts/fusion/plugins/create/create_saver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index f1e7791972..04898d0a45 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -233,7 +233,7 @@ class CreateSaver(NewCreator): def _get_frame_range_enum(self): frame_range_options = { "asset_db": "Current asset context", - "render_range": "From viewer render in/out", + "render_range": "From render in/out", "comp_range": "From composition timeline" } From 1b3b7b1a737004416042c00f8ab48ac263200351 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 23 May 2023 12:41:33 +0200 Subject: [PATCH 106/189] Render instances with their explicit frame ranges --- .../plugins/publish/extract_render_local.py | 125 ++++++++++++------ 1 file changed, 85 insertions(+), 40 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/extract_render_local.py b/openpype/hosts/fusion/plugins/publish/extract_render_local.py index f801f30577..1663ca04fa 100644 --- a/openpype/hosts/fusion/plugins/publish/extract_render_local.py +++ b/openpype/hosts/fusion/plugins/publish/extract_render_local.py @@ -1,6 +1,7 @@ import os import logging import contextlib +import collections import pyblish.api from openpype.pipeline import publish @@ -52,11 +53,14 @@ class FusionRenderLocal( hosts = ["fusion"] families = ["render.local"] + is_rendered_key = "_fusionrenderlocal_has_rendered" + def process(self, instance): - context = instance.context # Start render - self.render_once(context) + result = self.render(instance) + if result is False: + raise RuntimeError(f"Comp render failed for {instance}") self._add_representation(instance) @@ -69,52 +73,61 @@ class FusionRenderLocal( ) ) - def render_once(self, context): - """Render context comp only once, even with more render instances""" + def render(self, instance): + """Render instance. - # This plug-in assumes all render nodes get rendered at the same time - # to speed up the rendering. The check below makes sure that we only - # execute the rendering once and not for each instance. - key = f"__hasRun{self.__class__.__name__}" + We try to render the minimal amount of times by combining the instances + that have a matching frame range in one Fusion render. Then for the + batch of instances we store whether the - savers_to_render = [ - # Get the saver tool from the instance - instance.data["tool"] for instance in context if - # Only active instances - instance.data.get("publish", True) and - # Only render.local instances - "render.local" in instance.data.get("families", []) - ] + """ - if key not in context.data: - # We initialize as false to indicate it wasn't successful yet - # so we can keep track of whether Fusion succeeded - context.data[key] = False + if self.is_rendered_key in instance.data: + # This instance was already processed in batch with another + # instance, so we just return the render result directly + self.log.debug(f"Instance {instance} was already rendered") + return instance.data[self.is_rendered_key] - current_comp = context.data["currentComp"] - frame_start = context.data["frameStartHandle"] - frame_end = context.data["frameEndHandle"] + instances_by_frame_range = self.get_render_instances_by_frame_range( + instance.context + ) - self.log.info("Starting Fusion render") - self.log.info(f"Start frame: {frame_start}") - self.log.info(f"End frame: {frame_end}") - saver_names = ", ".join(saver.Name for saver in savers_to_render) - self.log.info(f"Rendering tools: {saver_names}") + # Render matching batch of instances that share the same frame range + frame_range = self.get_instance_render_frame_range(instance) + render_instances = instances_by_frame_range[frame_range] - with comp_lock_and_undo_chunk(current_comp): - with enabled_savers(current_comp, savers_to_render): - result = current_comp.Render( - { - "Start": frame_start, - "End": frame_end, - "Wait": True, - } - ) + # We initialize render state false to indicate it wasn't successful + # yet to keep track of whether Fusion succeeded. This is for cases + # where an error below this might cause the comp render result not + # to be stored for the instances of this batch + for render_instance in render_instances: + render_instance.data[self.is_rendered_key] = False - context.data[key] = bool(result) + savers_to_render = [inst.data["tool"] for inst in render_instances] + current_comp = instance.context.data["currentComp"] + frame_start, frame_end = frame_range - if context.data[key] is False: - raise RuntimeError("Comp render failed") + self.log.info( + f"Starting Fusion render frame range {frame_start}-{frame_end}" + ) + saver_names = ", ".join(saver.Name for saver in savers_to_render) + self.log.info(f"Rendering tools: {saver_names}") + + with comp_lock_and_undo_chunk(current_comp): + with enabled_savers(current_comp, savers_to_render): + result = current_comp.Render( + { + "Start": frame_start, + "End": frame_end, + "Wait": True, + } + ) + + # Store the render state for all the rendered instances + for render_instance in render_instances: + render_instance.data[self.is_rendered_key] = bool(result) + + return result def _add_representation(self, instance): """Add representation to instance""" @@ -151,3 +164,35 @@ class FusionRenderLocal( instance.data["representations"].append(repre) return instance + + def get_render_instances_by_frame_range(self, context): + """Return enabled render.local instances grouped by their frame range. + + Arguments: + context (pyblish.Context): The pyblish context + + Returns: + dict: (start, end): instances mapping + + """ + + instances_to_render = [ + instance for instance in context if + # Only active instances + instance.data.get("publish", True) and + # Only render.local instances + "render.local" in instance.data.get("families", []) + ] + + # Instances by frame ranges + instances_by_frame_range = collections.defaultdict(list) + for instance in instances_to_render: + start, end = self.get_instance_render_frame_range(instance) + instances_by_frame_range[(start, end)].append(instance) + + return dict(instances_by_frame_range) + + def get_instance_render_frame_range(self, instance): + start = instance.data["frameStartHandle"] + end = instance.data["frameEndHandle"] + return start, end From a2007887299de2f65b21e93522fbbe4de3277c63 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 23 May 2023 11:50:34 +0100 Subject: [PATCH 107/189] Removed get_full_name() calls because of unexpected behaviour --- openpype/hosts/unreal/plugins/load/load_animation.py | 4 ++-- openpype/hosts/unreal/plugins/load/load_camera.py | 6 +++--- openpype/hosts/unreal/plugins/load/load_layout.py | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_animation.py b/openpype/hosts/unreal/plugins/load/load_animation.py index 778ddf693d..a5ecb677e8 100644 --- a/openpype/hosts/unreal/plugins/load/load_animation.py +++ b/openpype/hosts/unreal/plugins/load/load_animation.py @@ -156,7 +156,7 @@ class AnimationFBXLoader(plugin.Loader): package_paths=[f"{root}/{hierarchy[0]}"], recursive_paths=False) levels = ar.get_assets(_filter) - master_level = levels[0].get_full_name() + master_level = levels[0].get_asset().get_path_name() hierarchy_dir = root for h in hierarchy: @@ -168,7 +168,7 @@ class AnimationFBXLoader(plugin.Loader): package_paths=[f"{hierarchy_dir}/"], recursive_paths=True) levels = ar.get_assets(_filter) - level = levels[0].get_full_name() + level = levels[0].get_asset().get_path_name() unreal.EditorLevelLibrary.save_all_dirty_levels() unreal.EditorLevelLibrary.load_level(level) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 1bd398349f..072b3b1467 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -365,7 +365,7 @@ class CameraLoader(plugin.Loader): maps = ar.get_assets(filter) # There should be only one map in the list - EditorLevelLibrary.load_level(maps[0].get_full_name()) + EditorLevelLibrary.load_level(maps[0].get_asset().get_path_name()) level_sequence = sequences[0].get_asset() @@ -513,7 +513,7 @@ class CameraLoader(plugin.Loader): map = maps[0] EditorLevelLibrary.save_all_dirty_levels() - EditorLevelLibrary.load_level(map.get_full_name()) + EditorLevelLibrary.load_level(map.get_asset().get_path_name()) # Remove the camera from the level. actors = EditorLevelLibrary.get_all_level_actors() @@ -523,7 +523,7 @@ class CameraLoader(plugin.Loader): EditorLevelLibrary.destroy_actor(a) EditorLevelLibrary.save_all_dirty_levels() - EditorLevelLibrary.load_level(world.get_full_name()) + EditorLevelLibrary.load_level(world.get_asset().get_path_name()) # There should be only one sequence in the path. sequence_name = sequences[0].asset_name diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index e5f32c3412..d94e6e5837 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -740,7 +740,7 @@ class LayoutLoader(plugin.Loader): loaded_assets = self._process(self.fname, asset_dir, shot) for s in sequences: - EditorAssetLibrary.save_asset(s.get_full_name()) + EditorAssetLibrary.save_asset(s.get_path_name()) EditorLevelLibrary.save_current_level() @@ -819,7 +819,7 @@ class LayoutLoader(plugin.Loader): recursive_paths=False) levels = ar.get_assets(filter) - layout_level = levels[0].get_full_name() + layout_level = levels[0].get_asset().get_path_name() EditorLevelLibrary.save_all_dirty_levels() EditorLevelLibrary.load_level(layout_level) @@ -919,7 +919,7 @@ class LayoutLoader(plugin.Loader): package_paths=[f"{root}/{ms_asset}"], recursive_paths=False) levels = ar.get_assets(_filter) - master_level = levels[0].get_full_name() + master_level = levels[0].get_asset().get_path_name() sequences = [master_sequence] From 915c0934854f53914013291ef7d91ee39a8a23f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Tue, 23 May 2023 13:55:58 +0200 Subject: [PATCH 108/189] Update openpype/hosts/resolve/hooks/pre_resolve_setup.py Co-authored-by: Roy Nieterau --- openpype/hosts/resolve/hooks/pre_resolve_setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/resolve/hooks/pre_resolve_setup.py b/openpype/hosts/resolve/hooks/pre_resolve_setup.py index 6747e773a3..d066fc2da2 100644 --- a/openpype/hosts/resolve/hooks/pre_resolve_setup.py +++ b/openpype/hosts/resolve/hooks/pre_resolve_setup.py @@ -101,7 +101,8 @@ class ResolvePrelaunch(PreLaunchHook): self.log.debug(f"PYTHONPATH: {self.launch_context.env['PYTHONPATH']}") - # add to the python path to PATH + # add the pythonhome folder to PATH because on Windows + # this is needed for Py3 to be correctly detected within Resolve env_path = self.launch_context.env["PATH"] self.log.info(f"Adding `{python3_home_str}` to the PATH variable") self.launch_context.env[ From 4f24356139425df00ad13e5dce943d739c2f63ee Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 23 May 2023 14:20:30 +0200 Subject: [PATCH 109/189] Add validator for instance frame range to be within comp global in/out --- .../publish/validate_instance_frame_range.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 openpype/hosts/fusion/plugins/publish/validate_instance_frame_range.py diff --git a/openpype/hosts/fusion/plugins/publish/validate_instance_frame_range.py b/openpype/hosts/fusion/plugins/publish/validate_instance_frame_range.py new file mode 100644 index 0000000000..06cd0ca186 --- /dev/null +++ b/openpype/hosts/fusion/plugins/publish/validate_instance_frame_range.py @@ -0,0 +1,41 @@ +import pyblish.api + +from openpype.pipeline import PublishValidationError + + +class ValidateInstanceFrameRange(pyblish.api.InstancePlugin): + """Validate instance frame range is within comp's global render range.""" + + order = pyblish.api.ValidatorOrder + label = "Validate Filename Has Extension" + families = ["render"] + hosts = ["fusion"] + + def process(self, instance): + + context = instance.context + global_start = context.data["compFrameStart"] + global_end = context.data["compFrameEnd"] + + render_start = instance.data["frameStartHandle"] + render_end = instance.data["frameEndHandle"] + + if render_start < global_start or render_end > global_end: + + message = ( + f"Instance {instance} render frame range " + f"({render_start}-{render_end}) is outside of the comp's " + f"global render range ({global_start}-{global_end}) and thus " + f"can't be rendered. " + ) + description = ( + f"{message}\n\n" + f"Either update the comp's global range or the instance's " + f"frame range to ensure the comp's frame range includes the " + f"to render frame range for the instance." + ) + raise PublishValidationError( + title="Frame range outside of comp range", + message=message, + description=description + ) From af6ce0bf9fd9ac34f9afb7dfd39168f3258ea76f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 23 May 2023 14:21:38 +0200 Subject: [PATCH 110/189] Fix docstring --- openpype/hosts/fusion/plugins/publish/extract_render_local.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/publish/extract_render_local.py b/openpype/hosts/fusion/plugins/publish/extract_render_local.py index 1663ca04fa..564dca1796 100644 --- a/openpype/hosts/fusion/plugins/publish/extract_render_local.py +++ b/openpype/hosts/fusion/plugins/publish/extract_render_local.py @@ -78,7 +78,7 @@ class FusionRenderLocal( We try to render the minimal amount of times by combining the instances that have a matching frame range in one Fusion render. Then for the - batch of instances we store whether the + batch of instances we store whether the render succeeded or failed. """ From 81d41bb0edbd74f8196072c59f773592c94242f8 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 23 May 2023 16:18:58 +0200 Subject: [PATCH 111/189] fixing frame range data passing from instance --- .../hosts/fusion/plugins/publish/collect_render.py | 6 ++++-- .../pipeline/publish/abstract_collect_render.py | 13 ++++++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py index 551a365099..a20a142701 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_render.py +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -17,6 +17,8 @@ class FusionRenderInstance(RenderInstance): tool = attr.ib(default=None) workfileComp = attr.ib(default=None) publish_attributes = attr.ib(default={}) + frameStartHandle = attr.ib(default=None) + frameEndHandle = attr.ib(default=None) class CollectFusionRender( @@ -83,8 +85,8 @@ class CollectFusionRender( frameEnd=inst.data["frameEnd"], handleStart=inst.data["handleStart"], handleEnd=inst.data["handleEnd"], - ignoreFrameHandleCheck=( - inst.data["frame_range_source"] == "asset_db"), + frameStartHandle=inst.data["frameStartHandle"], + frameEndHandle=inst.data["frameEndHandle"], frameStep=1, fps=comp_frame_format_prefs.get("Rate"), app_version=comp.GetApp().Version, diff --git a/openpype/pipeline/publish/abstract_collect_render.py b/openpype/pipeline/publish/abstract_collect_render.py index fd35ddb719..1e392d25e3 100644 --- a/openpype/pipeline/publish/abstract_collect_render.py +++ b/openpype/pipeline/publish/abstract_collect_render.py @@ -167,16 +167,27 @@ class AbstractCollectRender(pyblish.api.ContextPlugin): frame_start_render = int(render_instance.frameStart) frame_end_render = int(render_instance.frameEnd) + """ TODO: Needs to be refofactored because this + seems to be very hacky + """ if (render_instance.ignoreFrameHandleCheck or int(context.data['frameStartHandle']) == frame_start_render and int(context.data['frameEndHandle']) == frame_end_render): # noqa: W503, E501 - + # only for Harmony where frame range cannot be set by DB handle_start = context.data['handleStart'] handle_end = context.data['handleEnd'] frame_start = context.data['frameStart'] frame_end = context.data['frameEnd'] frame_start_handle = context.data['frameStartHandle'] frame_end_handle = context.data['frameEndHandle'] + elif (hasattr(render_instance, "frameStartHandle") + and hasattr(render_instance, "frameEndHandle")): + handle_start = int(render_instance.handleStart) + handle_end = int(render_instance.handleEnd) + frame_start = int(render_instance.frameStart) + frame_end = int(render_instance.frameEnd) + frame_start_handle = int(render_instance.frameStartHandle) + frame_end_handle = int(render_instance.frameEndHandle) else: handle_start = 0 handle_end = 0 From 561b4cb5d52e2f201a82f029a09b9b450a02085b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Tue, 23 May 2023 16:31:28 +0200 Subject: [PATCH 112/189] Update openpype/pipeline/publish/abstract_collect_render.py Co-authored-by: Roy Nieterau --- openpype/pipeline/publish/abstract_collect_render.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/pipeline/publish/abstract_collect_render.py b/openpype/pipeline/publish/abstract_collect_render.py index 1e392d25e3..6877d556c3 100644 --- a/openpype/pipeline/publish/abstract_collect_render.py +++ b/openpype/pipeline/publish/abstract_collect_render.py @@ -167,9 +167,7 @@ class AbstractCollectRender(pyblish.api.ContextPlugin): frame_start_render = int(render_instance.frameStart) frame_end_render = int(render_instance.frameEnd) - """ TODO: Needs to be refofactored because this - seems to be very hacky - """ + # TODO: Refactor hacky frame range workaround below if (render_instance.ignoreFrameHandleCheck or int(context.data['frameStartHandle']) == frame_start_render and int(context.data['frameEndHandle']) == frame_end_render): # noqa: W503, E501 From 902e6a9b61a7290e910c30009beb23dad9337c58 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 23 May 2023 15:54:11 +0100 Subject: [PATCH 113/189] Long names fixes. --- .../hosts/maya/plugins/publish/validate_rig_output_ids.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py b/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py index 499bfd4e37..cba70a21b7 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py @@ -55,7 +55,8 @@ class ValidateRigOutputIds(pyblish.api.InstancePlugin): if shapes: instance_nodes.extend(shapes) - scene_nodes = cmds.ls(type="transform") + cmds.ls(type="mesh") + scene_nodes = cmds.ls(type="transform", long=True) + scene_nodes += cmds.ls(type="mesh", long=True) scene_nodes = set(scene_nodes) - set(instance_nodes) scene_nodes_by_basename = defaultdict(list) @@ -76,7 +77,7 @@ class ValidateRigOutputIds(pyblish.api.InstancePlugin): if len(ids) > 1: cls.log.error( "\"{}\" id mismatch to: {}".format( - instance_node.longName(), matches + instance_node, matches ) ) invalid[instance_node] = matches From b70051b768e1b4b126b3f17fd2a734ab486edc58 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 23 May 2023 17:55:54 +0200 Subject: [PATCH 114/189] Preserve comp frame range after rendering --- openpype/hosts/fusion/api/lib.py | 33 +++++++++++++++++-- .../plugins/publish/extract_render_local.py | 19 ++++++----- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/fusion/api/lib.py b/openpype/hosts/fusion/api/lib.py index c33209823e..1c486783be 100644 --- a/openpype/hosts/fusion/api/lib.py +++ b/openpype/hosts/fusion/api/lib.py @@ -256,8 +256,11 @@ def switch_item(container, @contextlib.contextmanager -def maintained_selection(): - comp = get_current_comp() +def maintained_selection(comp=None): + """Reset comp selection from before the context after the context""" + if comp is None: + comp = get_current_comp() + previous_selection = comp.GetToolList(True).values() try: yield @@ -268,6 +271,32 @@ def maintained_selection(): for tool in previous_selection: flow.Select(tool, True) +@contextlib.contextmanager +def maintained_comp_range(comp=None, + global_start=True, + global_end=True, + render_start=True, + render_end=True): + """Reset comp frame ranges from before the context after the context""" + if comp is None: + comp = get_current_comp() + + comp_attrs = comp.GetAttrs() + preserve_attrs = {} + if global_start: + preserve_attrs["COMPN_GlobalStart"] = comp_attrs["COMPN_GlobalStart"] + if global_end: + preserve_attrs["COMPN_GlobalEnd"] = comp_attrs["COMPN_GlobalEnd"] + if render_start: + preserve_attrs["COMPN_RenderStart"] = comp_attrs["COMPN_RenderStart"] + if render_end: + preserve_attrs["COMPN_RenderEnd"] = comp_attrs["COMPN_RenderEnd"] + + try: + yield + finally: + comp.SetAttrs(preserve_attrs) + def get_frame_path(path): """Get filename for the Fusion Saver with padded number as '#' diff --git a/openpype/hosts/fusion/plugins/publish/extract_render_local.py b/openpype/hosts/fusion/plugins/publish/extract_render_local.py index 564dca1796..25c101cf00 100644 --- a/openpype/hosts/fusion/plugins/publish/extract_render_local.py +++ b/openpype/hosts/fusion/plugins/publish/extract_render_local.py @@ -6,7 +6,7 @@ import pyblish.api from openpype.pipeline import publish from openpype.hosts.fusion.api import comp_lock_and_undo_chunk -from openpype.hosts.fusion.api.lib import get_frame_path +from openpype.hosts.fusion.api.lib import get_frame_path, maintained_comp_range log = logging.getLogger(__name__) @@ -114,14 +114,15 @@ class FusionRenderLocal( self.log.info(f"Rendering tools: {saver_names}") with comp_lock_and_undo_chunk(current_comp): - with enabled_savers(current_comp, savers_to_render): - result = current_comp.Render( - { - "Start": frame_start, - "End": frame_end, - "Wait": True, - } - ) + with maintained_comp_range(current_comp): + with enabled_savers(current_comp, savers_to_render): + result = current_comp.Render( + { + "Start": frame_start, + "End": frame_end, + "Wait": True, + } + ) # Store the render state for all the rendered instances for render_instance in render_instances: From 409929c3b8233d922463410a12f452766ee94123 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 23 May 2023 17:57:23 +0200 Subject: [PATCH 115/189] Cosmetics --- openpype/hosts/fusion/api/lib.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/fusion/api/lib.py b/openpype/hosts/fusion/api/lib.py index 1c486783be..cba8c38c2f 100644 --- a/openpype/hosts/fusion/api/lib.py +++ b/openpype/hosts/fusion/api/lib.py @@ -271,6 +271,7 @@ def maintained_selection(comp=None): for tool in previous_selection: flow.Select(tool, True) + @contextlib.contextmanager def maintained_comp_range(comp=None, global_start=True, From a73d19b612c6c4d423a8784b0598e71263213c9f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 23 May 2023 18:16:05 +0200 Subject: [PATCH 116/189] Publisher: Show instances in report page (#4915) * renamed 'validations_widget.py' to 'report_page.py' * Implemented base logic and widgets for logs * make one report page * added missing imports * added missing constants * move and rename 'VerticallScrollArea' to 'VerticalScrollArea' * Validation erro item have id * use 'ReportPageWidget' in window * change 'bg-button-hover' key to 'bg-buttons-hover' in style colors * move publish actions widgets * Refactored how validation error title is showed * remove item id from validation error item but add id to group items * remove margins from actions widget * shrink publish frame on finished publishing * fix dash line draw * add missing styles * fix dash line in thumbnail widget * added crash widget and changed layout a little * added infor overlay message * export and copy report happens in main window * fix docstrings * added per plugin filtering for validation errors * added implementation of 'FlowLayout' * actions buttons are in flow layout * fix actions order * implemented expanding text edit widget * expand button has some signals and properties * description and details are separated widgets * fix typo * added constans to '__all__' * parse icon def is a function * change layout of widgets * fix log filtering * added state icon to instances * fix pyside6 issues * implemented 'ClassicExpandBtnLabel' with arrow images * modified details separator * added some spacing to layouts * fix syle of description inputs and progress color * removed unused import * add 'is_validation_error' to errored result * validation error has different icon in logs view * added plugin name to ValueError if happens * spacer before detail inputs moved out of detals widget * fix actions visible in craash report * ignore pyblish base classes * filter base plugins in discovery * use 'is' comparison instead of '__eq__' * fix action error handling * Fix handling of 'None' values in comparison * formatting fix * Report instance card have same margins as in create mode * publish instances are grouped by family * log messages are rstripped --- openpype/pipeline/publish/lib.py | 8 + openpype/style/data.json | 9 +- openpype/style/style.css | 64 +- openpype/tools/attribute_defs/files_widget.py | 24 +- openpype/tools/publisher/constants.py | 6 + openpype/tools/publisher/control.py | 41 +- .../publish_report_viewer/widgets.py | 6 +- openpype/tools/publisher/widgets/__init__.py | 4 +- .../publisher/widgets/card_view_widgets.py | 6 +- .../tools/publisher/widgets/images/error.png | Bin 0 -> 14667 bytes .../publisher/widgets/images/success.png | Bin 0 -> 14514 bytes .../publisher/widgets/images/warning.png | Bin 9748 -> 11546 bytes .../tools/publisher/widgets/publish_frame.py | 39 +- .../tools/publisher/widgets/report_page.py | 1876 +++++++++++++++++ .../publisher/widgets/thumbnail_widget.py | 21 +- .../publisher/widgets/validations_widget.py | 715 ------- openpype/tools/publisher/widgets/widgets.py | 65 +- openpype/tools/publisher/window.py | 58 +- openpype/tools/utils/__init__.py | 7 + openpype/tools/utils/layouts.py | 150 ++ openpype/tools/utils/widgets.py | 108 +- 21 files changed, 2373 insertions(+), 834 deletions(-) create mode 100644 openpype/tools/publisher/widgets/images/error.png create mode 100644 openpype/tools/publisher/widgets/images/success.png create mode 100644 openpype/tools/publisher/widgets/report_page.py delete mode 100644 openpype/tools/publisher/widgets/validations_widget.py create mode 100644 openpype/tools/utils/layouts.py diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index 080f93e514..40186238aa 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -320,6 +320,14 @@ def publish_plugins_discover(paths=None): continue for plugin in pyblish.plugin.plugins_from_module(module): + # Ignore base plugin classes + # NOTE 'pyblish.api.discover' does not ignore them! + if ( + plugin is pyblish.api.Plugin + or plugin is pyblish.api.ContextPlugin + or plugin is pyblish.api.InstancePlugin + ): + continue if not allow_duplicates and plugin.__name__ in plugin_names: result.duplicated_plugins.append(plugin) log.debug("Duplicate plug-in found: %s", plugin) diff --git a/openpype/style/data.json b/openpype/style/data.json index bea2a3d407..7389387d97 100644 --- a/openpype/style/data.json +++ b/openpype/style/data.json @@ -26,8 +26,8 @@ "bg": "#2C313A", "bg-inputs": "#21252B", - "bg-buttons": "#434a56", - "bg-button-hover": "rgb(81, 86, 97)", + "bg-buttons": "rgb(67, 74, 86)", + "bg-buttons-hover": "rgb(81, 86, 97)", "bg-inputs-disabled": "#2C313A", "bg-buttons-disabled": "#434a56", @@ -66,7 +66,9 @@ "bg-success": "#458056", "bg-success-hover": "#55a066", "bg-error": "#AD2E2E", - "bg-error-hover": "#C93636" + "bg-error-hover": "#C93636", + "bg-info": "rgb(63, 98, 121)", + "bg-info-hover": "rgb(81, 146, 181)" }, "tab-widget": { "bg": "#21252B", @@ -94,6 +96,7 @@ "crash": "#FF6432", "success": "#458056", "warning": "#ffc671", + "progress": "rgb(194, 226, 236)", "tab-bg": "#16191d", "list-view-group": { "bg": "#434a56", diff --git a/openpype/style/style.css b/openpype/style/style.css index 827b103f94..5ce55aa658 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -136,7 +136,7 @@ QPushButton { } QPushButton:hover { - background: {color:bg-button-hover}; + background: {color:bg-buttons-hover}; color: {color:font-hover}; } @@ -166,7 +166,7 @@ QToolButton { } QToolButton:hover { - background: {color:bg-button-hover}; + background: {color:bg-buttons-hover}; color: {color:font-hover}; } @@ -722,6 +722,13 @@ OverlayMessageWidget[type="error"]:hover { background: {color:overlay-messages:bg-error-hover}; } +OverlayMessageWidget[type="info"] { + background: {color:overlay-messages:bg-info}; +} +OverlayMessageWidget[type="info"]:hover { + background: {color:overlay-messages:bg-info-hover}; +} + OverlayMessageWidget QWidget { background: transparent; } @@ -749,10 +756,11 @@ OverlayMessageWidget QWidget { } #InfoText { - padding-left: 30px; - padding-top: 20px; + padding-left: 0px; + padding-top: 0px; + padding-right: 20px; background: transparent; - border: 1px solid {color:border}; + border: none; } #TypeEditor, #ToolEditor, #NameEditor, #NumberEditor { @@ -914,7 +922,7 @@ PixmapButton{ background: {color:bg-buttons}; } PixmapButton:hover { - background: {color:bg-button-hover}; + background: {color:bg-buttons-hover}; } PixmapButton:disabled { background: {color:bg-buttons-disabled}; @@ -925,7 +933,7 @@ PixmapButton:disabled { background: {color:bg-view}; } #ThumbnailPixmapHoverButton:hover { - background: {color:bg-button-hover}; + background: {color:bg-buttons-hover}; } #CreatorDetailedDescription { @@ -946,7 +954,7 @@ PixmapButton:disabled { } #CreateDialogHelpButton:hover { - background: {color:bg-button-hover}; + background: {color:bg-buttons-hover}; } #CreateDialogHelpButton QWidget { background: transparent; @@ -1005,7 +1013,7 @@ PixmapButton:disabled { border-radius: 0.2em; } #CardViewWidget:hover { - background: {color:bg-button-hover}; + background: {color:bg-buttons-hover}; } #CardViewWidget[state="selected"] { background: {color:bg-view-selection}; @@ -1032,7 +1040,7 @@ PixmapButton:disabled { } #PublishInfoFrame[state="3"], #PublishInfoFrame[state="4"] { - background: rgb(194, 226, 236); + background: {color:publisher:progress}; } #PublishInfoFrame QLabel { @@ -1040,6 +1048,11 @@ PixmapButton:disabled { font-style: bold; } +#PublishReportHeader { + font-size: 14pt; + font-weight: bold; +} + #PublishInfoMainLabel { font-size: 12pt; } @@ -1060,7 +1073,7 @@ ValidationArtistMessage QLabel { } #ValidationActionButton:hover { - background: {color:bg-button-hover}; + background: {color:bg-buttons-hover}; color: {color:font-hover}; } @@ -1090,6 +1103,35 @@ ValidationArtistMessage QLabel { border-left: 1px solid {color:border}; } +#PublishInstancesDetails { + border: 1px solid {color:border}; + border-radius: 0.3em; +} + +#InstancesLogsView { + border: 1px solid {color:border}; + background: {color:bg-view}; + border-radius: 0.3em; +} + +#PublishLogMessage { + font-family: "Noto Sans Mono"; +} + +#PublishInstanceLogsLabel { + font-weight: bold; +} + +#PublishCrashMainLabel{ + font-weight: bold; + font-size: 16pt; +} + +#PublishCrashReportLabel { + font-weight: bold; + font-size: 13pt; +} + #AssetNameInputWidget { background: {color:bg-inputs}; border: 1px solid {color:border}; diff --git a/openpype/tools/attribute_defs/files_widget.py b/openpype/tools/attribute_defs/files_widget.py index 067866035f..076b33fb7c 100644 --- a/openpype/tools/attribute_defs/files_widget.py +++ b/openpype/tools/attribute_defs/files_widget.py @@ -198,29 +198,33 @@ class DropEmpty(QtWidgets.QWidget): def paintEvent(self, event): super(DropEmpty, self).paintEvent(event) - painter = QtGui.QPainter(self) + pen = QtGui.QPen() - pen.setWidth(1) pen.setBrush(QtCore.Qt.darkGray) pen.setStyle(QtCore.Qt.DashLine) - painter.setPen(pen) - content_margins = self.layout().contentsMargins() + pen.setWidth(1) - left_m = content_margins.left() - top_m = content_margins.top() - rect = QtCore.QRect( + content_margins = self.layout().contentsMargins() + rect = self.rect() + left_m = content_margins.left() + pen.width() + top_m = content_margins.top() + pen.width() + new_rect = QtCore.QRect( left_m, top_m, ( - self.rect().width() + rect.width() - (left_m + content_margins.right() + pen.width()) ), ( - self.rect().height() + rect.height() - (top_m + content_margins.bottom() + pen.width()) ) ) - painter.drawRect(rect) + + painter = QtGui.QPainter(self) + painter.setRenderHint(QtGui.QPainter.Antialiasing) + painter.setPen(pen) + painter.drawRect(new_rect) class FilesModel(QtGui.QStandardItemModel): diff --git a/openpype/tools/publisher/constants.py b/openpype/tools/publisher/constants.py index 660fccecf1..4630eb144b 100644 --- a/openpype/tools/publisher/constants.py +++ b/openpype/tools/publisher/constants.py @@ -35,9 +35,13 @@ ResetKeySequence = QtGui.QKeySequence( __all__ = ( "CONTEXT_ID", + "CONTEXT_LABEL", "VARIANT_TOOLTIP", + "INPUTS_LAYOUT_HSPACING", + "INPUTS_LAYOUT_VSPACING", + "INSTANCE_ID_ROLE", "SORT_VALUE_ROLE", "IS_GROUP_ROLE", @@ -47,4 +51,6 @@ __all__ = ( "FAMILY_ROLE", "GROUP_ROLE", "CONVERTER_IDENTIFIER_ROLE", + + "ResetKeySequence", ) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 4b083d4bc8..8095d00103 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -47,6 +47,7 @@ PLUGIN_ORDER_OFFSET = 0.5 class CardMessageTypes: standard = None + info = "info" error = "error" @@ -220,7 +221,12 @@ class PublishReportMaker: def _add_plugin_data_item(self, plugin): if plugin in self._stored_plugins: - raise ValueError("Plugin is already stored") + # A plugin would be processed more than once. What can cause it: + # - there is a bug in controller + # - plugin class is imported into multiple files + # - this can happen even with base classes from 'pyblish' + raise ValueError( + "Plugin '{}' is already stored".format(str(plugin))) self._stored_plugins.append(plugin) @@ -239,6 +245,7 @@ class PublishReportMaker: label = plugin.label return { + "id": plugin.id, "name": plugin.__name__, "label": label, "order": plugin.order, @@ -324,7 +331,7 @@ class PublishReportMaker: "instances": instances_details, "context": self._extract_context_data(self._current_context), "crashed_file_paths": crashed_file_paths, - "id": str(uuid.uuid4()), + "id": uuid.uuid4().hex, "report_version": "1.0.0" } @@ -342,7 +349,9 @@ class PublishReportMaker: "label": instance.data.get("label"), "family": instance.data["family"], "families": instance.data.get("families") or [], - "exists": exists + "exists": exists, + "creator_identifier": instance.data.get("creator_identifier"), + "instance_id": instance.data.get("instance_id"), } def _extract_instance_log_items(self, result): @@ -388,8 +397,11 @@ class PublishReportMaker: exception = result.get("error") if exception: fname, line_no, func, exc = exception.traceback + # Action result does not have 'is_validation_error' + is_validation_error = result.get("is_validation_error", False) output.append({ "type": "error", + "is_validation_error": is_validation_error, "msg": str(exception), "filename": str(fname), "lineno": str(line_no), @@ -426,13 +438,15 @@ class PublishPluginsProxy: plugin_id = plugin.id plugins_by_id[plugin_id] = plugin - action_ids = set() + action_ids = [] action_ids_by_plugin_id[plugin_id] = action_ids actions = getattr(plugin, "actions", None) or [] for action in actions: action_id = action.id - action_ids.add(action_id) + if action_id in actions_by_id: + continue + action_ids.append(action_id) actions_by_id[action_id] = action self._plugins_by_id = plugins_by_id @@ -461,7 +475,7 @@ class PublishPluginsProxy: return plugin.id def get_plugin_action_items(self, plugin_id): - """Get plugin action items for plugin by it's id. + """Get plugin action items for plugin by its id. Args: plugin_id (str): Publish plugin id. @@ -568,7 +582,7 @@ class ValidationErrorItem: context_validation, title, description, - detail, + detail ): self.instance_id = instance_id self.instance_label = instance_label @@ -677,6 +691,8 @@ class PublishValidationErrorsReport: for title in titles: grouped_error_items.append({ + "id": uuid.uuid4().hex, + "plugin_id": plugin_id, "plugin_action_items": list(plugin_action_items), "error_items": error_items_by_title[title], "title": title @@ -2379,7 +2395,8 @@ class PublisherController(BasePublisherController): yield MainThreadItem(self.stop_publish) # Add plugin to publish report - self._publish_report.add_plugin_iter(plugin, self._publish_context) + self._publish_report.add_plugin_iter( + plugin, self._publish_context) # WARNING This is hack fix for optional plugins if not self._is_publish_plugin_active(plugin): @@ -2461,14 +2478,14 @@ class PublisherController(BasePublisherController): plugin, self._publish_context, instance ) - self._publish_report.add_result(result) - exception = result.get("error") if exception: + has_validation_error = False if ( isinstance(exception, PublishValidationError) and not self.publish_has_validated ): + has_validation_error = True self._add_validation_error(result) else: @@ -2482,6 +2499,10 @@ class PublisherController(BasePublisherController): self.publish_error_msg = msg self.publish_has_crashed = True + result["is_validation_error"] = has_validation_error + + self._publish_report.add_result(result) + self._publish_next_process() diff --git a/openpype/tools/publisher/publish_report_viewer/widgets.py b/openpype/tools/publisher/publish_report_viewer/widgets.py index dc449b6b69..02c9b63a4e 100644 --- a/openpype/tools/publisher/publish_report_viewer/widgets.py +++ b/openpype/tools/publisher/publish_report_viewer/widgets.py @@ -163,7 +163,11 @@ class ZoomPlainText(QtWidgets.QPlainTextEdit): super(ZoomPlainText, self).wheelEvent(event) return - degrees = float(event.delta()) / 8 + if hasattr(event, "angleDelta"): + delta = event.angleDelta().y() + else: + delta = event.delta() + degrees = float(delta) / 8 steps = int(ceil(degrees / 5)) self._scheduled_scalings += steps if (self._scheduled_scalings * steps < 0): diff --git a/openpype/tools/publisher/widgets/__init__.py b/openpype/tools/publisher/widgets/__init__.py index f18e6cc61e..87a5f3914a 100644 --- a/openpype/tools/publisher/widgets/__init__.py +++ b/openpype/tools/publisher/widgets/__init__.py @@ -18,7 +18,7 @@ from .help_widget import ( from .publish_frame import PublishFrame from .tabs_widget import PublisherTabsWidget from .overview_widget import OverviewWidget -from .validations_widget import ValidationsWidget +from .report_page import ReportPageWidget __all__ = ( @@ -40,5 +40,5 @@ __all__ = ( "PublisherTabsWidget", "OverviewWidget", - "ValidationsWidget", + "ReportPageWidget", ) diff --git a/openpype/tools/publisher/widgets/card_view_widgets.py b/openpype/tools/publisher/widgets/card_view_widgets.py index 13715bc73c..eae8e0420a 100644 --- a/openpype/tools/publisher/widgets/card_view_widgets.py +++ b/openpype/tools/publisher/widgets/card_view_widgets.py @@ -93,7 +93,7 @@ class BaseGroupWidget(QtWidgets.QWidget): return self._group def get_widget_by_item_id(self, item_id): - """Get instance widget by it's id.""" + """Get instance widget by its id.""" return self._widgets_by_id.get(item_id) @@ -702,8 +702,8 @@ class InstanceCardView(AbstractInstanceView): for group_name in sorted_group_names: group_icons = { - idenfier: self._controller.get_creator_icon(idenfier) - for idenfier in identifiers_by_group[group_name] + identifier: self._controller.get_creator_icon(identifier) + for identifier in identifiers_by_group[group_name] } if group_name in self._widgets_by_group: group_widget = self._widgets_by_group[group_name] diff --git a/openpype/tools/publisher/widgets/images/error.png b/openpype/tools/publisher/widgets/images/error.png new file mode 100644 index 0000000000000000000000000000000000000000..7b09a57d7dff44b31b5f18746fa22a42b7d9f15d GIT binary patch literal 14667 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%0DjRy{e1C?ePOpy6!-*e$z;|Xnn(?D zpXQkrLy+DjASj+-wx6t?@ebwUR`0uxctB97l~{FM{J+PyxI!(i%57)%H{wYr^v%Sc zuD@pzD{>YcX>>qj2wnglaUl5^|1)Jgx$pYJQuoBT27 z!HS(8dUMi^=z60EypoUky_~5%M$mxL8 z2R1h>A<69oL)PUk=F_}VsHXhV+s);w-(V*MX|aT0=GY2jm{4v#fZN-9d$Km~UqOX; zb4%faGqpee0khM`5FU*t_mgJ9$((#;S(c_SEOkqj11tove#5k(1E=MV zrt04k$G+Mx3_*o})b*Wd0Tx^67_|8Kbh~i57CU1&5hHe!NzG}N+55G7pG6cut$r(O z4`N7-ISsy2R9lg_^yq^L@IC1WEefb-@J^4rG0Uu=!QK&opnN zp9Ov5g677MUB?iLBadN4ze7gkKA(A3dw?-YbCO;^yE3E{k;+^}a=ZkyG3Er=WF2o% z_YoURGSAYFvox+4-E*~yQ2uE&P1>*h6y9kqk)6)^p_|)Dos;y%7NLy3$FvKj-p6qF zKbBh{;^VXEG-<+f=CI~1SATBqWEYk1_F^4+7DpOS@4{m7{!fL6P`W!M?$F{i_asl~ zQ852cI~rG;wI*`@!?9b@rR6o>c!JHN9iNnJ$}t!@#|ub$Plbj!n|9?_*e1zVzXp!< zkGb~@8NgR?I-Mzs=&?{Y0oZz`a za)sOnG`CdsbCC;y2$yf?K)_2})y=DD(K9a^h>ay?D@VxO4xY8!JkA6P@!|`(Naao) zz=C{oXdbkqw0X)xO)=U?V-Aq#6o8F5y{N`k=dA#023iQ}db)3j3&;G{ z?w@D<_AUX&p~aE|uZ zR?*dVYJ($J-9kr$N(2W5bWQNVL%zu=$)*07y{&LyzIKC)?Z}!t!CuxTQ*X5&j8||HAB_g{)ipOgnE^QtdUK57#q#rTJRX|9ioH!!{ zT42+F$UZ!5EC^V$_fW~>rJ=mnhS2w^$-D?E#Vted$&DFTMVO8X&uaM zFKtAPP<8iv?z{TS(6+$8 zsG#&F1r|-_tV=odOOeSW_LJ@!0E+tM{=d1zt>}-+*Psc&qiuBPmd10&gDOl7c{?AV zwaDkqZ91*NP3n#wTOg=7svLK5C8N{PfyeU zIJHO?boR3Z+?3n^qF?ayxa3#KoZzuXH{$0#PEr?1fcd!LTk;LR=?OfLov4PjNr0}N z(+5#zu)TKSRR`EJcWL2!Tcs{Jd-9^I^vgph=Q=_;71;2hO z5~HB@*;fYUBlW5atjaO=P&6pEr^WE-0=uFwD_VXpOD!(SK;?C;M zCdwc~`WSCt`V{cn=~>ZG%jk1YaP2~jBBQ1j zq`W5VQ201AOgVy8RU99DCzDjeXPFh?dnBw;>&Y-RE+$92Bo%Gf98o5Yp9m?a*lG-dU4LeVWjy^;ab* zkMsD z#hFH5zT)j5cpQB2jvMqK#D}Fdc|U8TZFkw}W+B|ApQ}lwMdno*|L)h&`WHq^VC$!m zef-xD(*iat+noRFk`41}j&uLAG{=P-Ki)VVua-Hy8I>Nl;qc>4&P*WUwqt{kBOBd3 zgZmL#)RHUi;-%8RJ?OcvOc~7{q|t?Rm4y&Bf7V!LQeGB|}rpv{T`-Q!kF=hAMZ=WmoNg-)j+rm`& z1nqA*`7e=1A|K8G-su!9bYkSz%{vybEp1(G7Tjin1#aG{(5i#7tCr$RJOx_?3$E|2 zx+H-)EqQpgws;E#-YSFpN;BGsA>JX<(1V}m@cN0i>YUw*!-_Ozzo=rgFKD5$kAV*T zZd(x$%bnWt(o6^mCGKL6@Pe3!pW9s7c@RAO#hbPz!yOZv?fUrwcAzY<3RBD3j}#JQ zd3bGY9_@P!l`lIbzqr18a(gW|A%07PQ+LHYA%X42(EsQD81vPM&7IXwpVa_16%lan O4Z@q-n^l_-F8mKWCTVE^ literal 0 HcmV?d00001 diff --git a/openpype/tools/publisher/widgets/images/warning.png b/openpype/tools/publisher/widgets/images/warning.png index 76d1e34b6ccf951b166db53cd59d8183cded7586..531f62b741804a2fa48769c17f9e29fbb0cd5429 100644 GIT binary patch literal 11546 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 E3l)7W-~a#s diff --git a/openpype/tools/publisher/widgets/publish_frame.py b/openpype/tools/publisher/widgets/publish_frame.py index e4e6740532..d21130deff 100644 --- a/openpype/tools/publisher/widgets/publish_frame.py +++ b/openpype/tools/publisher/widgets/publish_frame.py @@ -468,45 +468,14 @@ class PublishFrame(QtWidgets.QWidget): widget.setProperty("state", state) widget.style().polish(widget) - def _copy_report(self): - logs = self._controller.get_publish_report() - logs_string = json.dumps(logs, indent=4) - - mime_data = QtCore.QMimeData() - mime_data.setText(logs_string) - QtWidgets.QApplication.instance().clipboard().setMimeData( - mime_data - ) - - def _export_report(self): - default_filename = "publish-report-{}".format( - time.strftime("%y%m%d-%H-%M") - ) - default_filepath = os.path.join( - os.path.expanduser("~"), - default_filename - ) - new_filepath, ext = QtWidgets.QFileDialog.getSaveFileName( - self, "Save report", default_filepath, ".json" - ) - if not ext or not new_filepath: - return - - logs = self._controller.get_publish_report() - full_path = new_filepath + ext - dir_path = os.path.dirname(full_path) - if not os.path.exists(dir_path): - os.makedirs(dir_path) - - with open(full_path, "w") as file_stream: - json.dump(logs, file_stream) - def _on_report_triggered(self, identifier): if identifier == "export_report": - self._export_report() + self._controller.event_system.emit( + "export_report.request", {}, "publish_frame") elif identifier == "copy_report": - self._copy_report() + self._controller.event_system.emit( + "copy_report.request", {}, "publish_frame") elif identifier == "go_to_report": self.details_page_requested.emit() diff --git a/openpype/tools/publisher/widgets/report_page.py b/openpype/tools/publisher/widgets/report_page.py new file mode 100644 index 0000000000..50a619f0a8 --- /dev/null +++ b/openpype/tools/publisher/widgets/report_page.py @@ -0,0 +1,1876 @@ +# -*- coding: utf-8 -*- +import collections +import logging + +try: + import commonmark +except Exception: + commonmark = None + +from qtpy import QtWidgets, QtCore, QtGui + +from openpype.style import get_objected_colors +from openpype.tools.utils import ( + BaseClickableFrame, + ClickableFrame, + ExpandingTextEdit, + FlowLayout, + ClassicExpandBtn, + paint_image_with_color, + SeparatorWidget, +) +from .widgets import IconValuePixmapLabel +from .icons import ( + get_pixmap, + get_image, +) +from ..constants import ( + INSTANCE_ID_ROLE, + CONTEXT_ID, + CONTEXT_LABEL, +) + +LOG_DEBUG_VISIBLE = 1 << 0 +LOG_INFO_VISIBLE = 1 << 1 +LOG_WARNING_VISIBLE = 1 << 2 +LOG_ERROR_VISIBLE = 1 << 3 +LOG_CRITICAL_VISIBLE = 1 << 4 +ERROR_VISIBLE = 1 << 5 +INFO_VISIBLE = 1 << 6 + + +class VerticalScrollArea(QtWidgets.QScrollArea): + """Scroll area for validation error titles. + + The biggest difference is that the scroll area has scroll bar on left side + and resize of content will also resize scrollarea itself. + + Resize if deferred by 100ms because at the moment of resize are not yet + propagated sizes and visibility of scroll bars. + """ + + def __init__(self, *args, **kwargs): + super(VerticalScrollArea, self).__init__(*args, **kwargs) + + self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) + self.setLayoutDirection(QtCore.Qt.RightToLeft) + + self.setAttribute(QtCore.Qt.WA_TranslucentBackground) + # Background of scrollbar will be transparent + scrollbar_bg = self.verticalScrollBar().parent() + if scrollbar_bg: + scrollbar_bg.setAttribute(QtCore.Qt.WA_TranslucentBackground) + self.setViewportMargins(0, 0, 0, 0) + + self.verticalScrollBar().installEventFilter(self) + + # Timer with 100ms offset after changing size + size_changed_timer = QtCore.QTimer() + size_changed_timer.setInterval(100) + size_changed_timer.setSingleShot(True) + + size_changed_timer.timeout.connect(self._on_timer_timeout) + self._size_changed_timer = size_changed_timer + + def setVerticalScrollBar(self, widget): + old_widget = self.verticalScrollBar() + if old_widget: + old_widget.removeEventFilter(self) + + super(VerticalScrollArea, self).setVerticalScrollBar(widget) + if widget: + widget.installEventFilter(self) + + def setWidget(self, widget): + old_widget = self.widget() + if old_widget: + old_widget.removeEventFilter(self) + + super(VerticalScrollArea, self).setWidget(widget) + if widget: + widget.installEventFilter(self) + + def _on_timer_timeout(self): + width = self.widget().width() + if self.verticalScrollBar().isVisible(): + width += self.verticalScrollBar().width() + self.setMinimumWidth(width) + + def eventFilter(self, obj, event): + if ( + event.type() == QtCore.QEvent.Resize + and (obj is self.widget() or obj is self.verticalScrollBar()) + ): + self._size_changed_timer.start() + return super(VerticalScrollArea, self).eventFilter(obj, event) + + +# --- Publish actions widget --- +class ActionButton(BaseClickableFrame): + """Plugin's action callback button. + + Action may have label or icon or both. + + Args: + plugin_action_item (PublishPluginActionItem): Action item that can be + triggered by its id. + """ + + action_clicked = QtCore.Signal(str, str) + + def __init__(self, plugin_action_item, parent): + super(ActionButton, self).__init__(parent) + + self.setObjectName("ValidationActionButton") + + self.plugin_action_item = plugin_action_item + + action_label = plugin_action_item.label + action_icon = plugin_action_item.icon + label_widget = QtWidgets.QLabel(action_label, self) + icon_label = None + if action_icon: + icon_label = IconValuePixmapLabel(action_icon, self) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(5, 0, 5, 0) + layout.addWidget(label_widget, 1) + if icon_label: + layout.addWidget(icon_label, 0) + + self.setSizePolicy( + QtWidgets.QSizePolicy.Minimum, + self.sizePolicy().verticalPolicy() + ) + + def _mouse_release_callback(self): + self.action_clicked.emit( + self.plugin_action_item.plugin_id, + self.plugin_action_item.action_id + ) + + +class ValidateActionsWidget(QtWidgets.QFrame): + """Wrapper widget for plugin actions. + + Change actions based on selected validation error. + """ + + def __init__(self, controller, parent): + super(ValidateActionsWidget, self).__init__(parent) + + self.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + content_widget = QtWidgets.QWidget(self) + content_layout = FlowLayout(content_widget) + content_layout.setContentsMargins(0, 0, 0, 0) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(content_widget) + + self._controller = controller + self._content_widget = content_widget + self._content_layout = content_layout + + self._actions_mapping = {} + + self._visible_mode = True + + def _update_visibility(self): + self.setVisible( + self._visible_mode + and self._content_layout.count() > 0 + ) + + def set_visible_mode(self, visible): + if self._visible_mode is visible: + return + self._visible_mode = visible + self._update_visibility() + + def _clear(self): + """Remove actions from widget.""" + while self._content_layout.count(): + item = self._content_layout.takeAt(0) + widget = item.widget() + if widget: + widget.setVisible(False) + widget.deleteLater() + self._actions_mapping = {} + + def set_error_info(self, error_info): + """Set selected plugin and show it's actions. + + Clears current actions from widget and recreate them from the plugin. + + Args: + Dict[str, Any]: Object holding error items, title and possible + actions to run. + """ + + self._clear() + + if not error_info: + self.setVisible(False) + return + + plugin_action_items = error_info["plugin_action_items"] + for plugin_action_item in plugin_action_items: + if not plugin_action_item.active: + continue + + if plugin_action_item.on_filter not in ("failed", "all"): + continue + + action_id = plugin_action_item.action_id + self._actions_mapping[action_id] = plugin_action_item + + action_btn = ActionButton(plugin_action_item, self._content_widget) + action_btn.action_clicked.connect(self._on_action_click) + self._content_layout.addWidget(action_btn) + + self._update_visibility() + + def _on_action_click(self, plugin_id, action_id): + self._controller.run_action(plugin_id, action_id) + + +# --- Validation error titles --- +class ValidationErrorInstanceList(QtWidgets.QListView): + """List of publish instances that caused a validation error. + + Instances are collected per plugin's validation error title. + """ + def __init__(self, *args, **kwargs): + super(ValidationErrorInstanceList, self).__init__(*args, **kwargs) + + self.setObjectName("ValidationErrorInstanceList") + + self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) + + def minimumSizeHint(self): + return self.sizeHint() + + def sizeHint(self): + result = super(ValidationErrorInstanceList, self).sizeHint() + row_count = self.model().rowCount() + height = 0 + if row_count > 0: + height = self.sizeHintForRow(0) * row_count + result.setHeight(height) + return result + + +class ValidationErrorTitleWidget(QtWidgets.QWidget): + """Title of validation error. + + Widget is used as radio button so requires clickable functionality and + changing style on selection/deselection. + + Has toggle button to show/hide instances on which validation error happened + if there is a list (Valdation error may happen on context). + """ + + selected = QtCore.Signal(str) + instance_changed = QtCore.Signal(str) + + def __init__(self, title_id, error_info, parent): + super(ValidationErrorTitleWidget, self).__init__(parent) + + self._title_id = title_id + self._error_info = error_info + self._selected = False + + title_frame = ClickableFrame(self) + title_frame.setObjectName("ValidationErrorTitleFrame") + + toggle_instance_btn = QtWidgets.QToolButton(title_frame) + toggle_instance_btn.setObjectName("ArrowBtn") + toggle_instance_btn.setArrowType(QtCore.Qt.RightArrow) + toggle_instance_btn.setMaximumWidth(14) + + label_widget = QtWidgets.QLabel(error_info["title"], title_frame) + + title_frame_layout = QtWidgets.QHBoxLayout(title_frame) + title_frame_layout.addWidget(label_widget, 1) + title_frame_layout.addWidget(toggle_instance_btn, 0) + + instances_model = QtGui.QStandardItemModel() + + instance_ids = [] + + items = [] + context_validation = False + for error_item in error_info["error_items"]: + context_validation = error_item.context_validation + if context_validation: + toggle_instance_btn.setArrowType(QtCore.Qt.NoArrow) + instance_ids.append(CONTEXT_ID) + # Add fake item to have minimum size hint of view widget + items.append(QtGui.QStandardItem(CONTEXT_LABEL)) + continue + + label = error_item.instance_label + item = QtGui.QStandardItem(label) + item.setFlags( + QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable + ) + item.setData(label, QtCore.Qt.ToolTipRole) + item.setData(error_item.instance_id, INSTANCE_ID_ROLE) + items.append(item) + instance_ids.append(error_item.instance_id) + + if items: + root_item = instances_model.invisibleRootItem() + root_item.appendRows(items) + + instances_view = ValidationErrorInstanceList(self) + instances_view.setModel(instances_model) + + self.setLayoutDirection(QtCore.Qt.LeftToRight) + + view_widget = QtWidgets.QWidget(self) + view_layout = QtWidgets.QHBoxLayout(view_widget) + view_layout.setContentsMargins(0, 0, 0, 0) + view_layout.setSpacing(0) + view_layout.addSpacing(14) + view_layout.addWidget(instances_view, 0) + + layout = QtWidgets.QVBoxLayout(self) + layout.setSpacing(0) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(title_frame, 0) + layout.addWidget(view_widget, 0) + view_widget.setVisible(False) + + if not context_validation: + toggle_instance_btn.clicked.connect(self._on_toggle_btn_click) + + title_frame.clicked.connect(self._mouse_release_callback) + instances_view.selectionModel().selectionChanged.connect( + self._on_selection_change + ) + + self._title_frame = title_frame + + self._toggle_instance_btn = toggle_instance_btn + + self._view_widget = view_widget + + self._instances_model = instances_model + self._instances_view = instances_view + + self._context_validation = context_validation + + self._instance_ids = instance_ids + self._expanded = False + + def sizeHint(self): + result = super(ValidationErrorTitleWidget, self).sizeHint() + expected_width = max( + self._view_widget.minimumSizeHint().width(), + self._view_widget.sizeHint().width() + ) + + if expected_width < 200: + expected_width = 200 + + if result.width() < expected_width: + result.setWidth(expected_width) + + return result + + def minimumSizeHint(self): + return self.sizeHint() + + def _mouse_release_callback(self): + """Mark this widget as selected on click.""" + + self.set_selected(True) + + @property + def is_selected(self): + """Is widget marked a selected. + + Returns: + bool: Item is selected or not. + """ + + return self._selected + + @property + def id(self): + return self._title_id + + def _change_style_property(self, selected): + """Change style of widget based on selection.""" + + value = "1" if selected else "" + self._title_frame.setProperty("selected", value) + self._title_frame.style().polish(self._title_frame) + + def set_selected(self, selected=None): + """Change selected state of widget.""" + + if selected is None: + selected = not self._selected + + # Clear instance view selection on deselect + if not selected: + self._instances_view.clearSelection() + + # Skip if has same value + if selected == self._selected: + return + + self._selected = selected + self._change_style_property(selected) + if selected: + self.selected.emit(self._title_id) + self._set_expanded(True) + + def _on_toggle_btn_click(self): + """Show/hide instances list.""" + + self._set_expanded() + + def _set_expanded(self, expanded=None): + if expanded is None: + expanded = not self._expanded + + elif expanded is self._expanded: + return + + if expanded and self._context_validation: + return + + self._expanded = expanded + self._view_widget.setVisible(expanded) + if expanded: + self._toggle_instance_btn.setArrowType(QtCore.Qt.DownArrow) + else: + self._toggle_instance_btn.setArrowType(QtCore.Qt.RightArrow) + + def _on_selection_change(self): + self.instance_changed.emit(self._title_id) + + def get_selected_instances(self): + if self._context_validation: + return [CONTEXT_ID] + sel_model = self._instances_view.selectionModel() + return [ + index.data(INSTANCE_ID_ROLE) + for index in sel_model.selectedIndexes() + if index.isValid() + ] + + def get_available_instances(self): + return list(self._instance_ids) + + +class ValidationArtistMessage(QtWidgets.QWidget): + def __init__(self, message, parent): + super(ValidationArtistMessage, self).__init__(parent) + + artist_msg_label = QtWidgets.QLabel(message, self) + artist_msg_label.setAlignment(QtCore.Qt.AlignCenter) + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget( + artist_msg_label, 1, QtCore.Qt.AlignCenter + ) + + +class ValidationErrorsView(QtWidgets.QWidget): + selection_changed = QtCore.Signal() + + def __init__(self, parent): + super(ValidationErrorsView, self).__init__(parent) + + errors_scroll = VerticalScrollArea(self) + errors_scroll.setWidgetResizable(True) + + errors_widget = QtWidgets.QWidget(errors_scroll) + errors_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + errors_scroll.setWidget(errors_widget) + + errors_layout = QtWidgets.QVBoxLayout(errors_widget) + errors_layout.setContentsMargins(0, 0, 0, 0) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(errors_scroll, 1) + + self._errors_widget = errors_widget + self._errors_layout = errors_layout + self._title_widgets = {} + self._previous_select = None + + def _clear(self): + """Delete all dynamic widgets and hide all wrappers.""" + + self._title_widgets = {} + self._previous_select = None + while self._errors_layout.count(): + item = self._errors_layout.takeAt(0) + widget = item.widget() + if widget: + widget.deleteLater() + + def set_errors(self, grouped_error_items): + """Set errors into context and created titles. + + Args: + validation_error_report (PublishValidationErrorsReport): Report + with information about validation errors and publish plugin + actions. + """ + + self._clear() + + first_id = None + for title_item in grouped_error_items: + title_id = title_item["id"] + if first_id is None: + first_id = title_id + widget = ValidationErrorTitleWidget(title_id, title_item, self) + widget.selected.connect(self._on_select) + widget.instance_changed.connect(self._on_instance_change) + self._errors_layout.addWidget(widget) + self._title_widgets[title_id] = widget + + self._errors_layout.addStretch(1) + + if first_id: + self._title_widgets[first_id].set_selected(True) + else: + self.selection_changed.emit() + + self.updateGeometry() + + def _on_select(self, title_id): + if self._previous_select: + if self._previous_select.id == title_id: + return + self._previous_select.set_selected(False) + + self._previous_select = self._title_widgets[title_id] + self.selection_changed.emit() + + def _on_instance_change(self, title_id): + if self._previous_select and self._previous_select.id != title_id: + self._title_widgets[title_id].set_selected(True) + else: + self.selection_changed.emit() + + def get_selected_items(self): + if not self._previous_select: + return None, [] + + title_id = self._previous_select.id + instance_ids = self._previous_select.get_selected_instances() + if not instance_ids: + instance_ids = self._previous_select.get_available_instances() + return title_id, instance_ids + + +# ----- Publish instance report ----- +class _InstanceItem: + """Publish instance item for report UI. + + Contains only data related to an instance in publishing. Has implemented + sorting methods and prepares information, e.g. if contains error or + warnings. + """ + + _attrs = ( + "creator_identifier", + "family", + "label", + "name", + ) + + def __init__( + self, + instance_id, + creator_identifier, + family, + name, + label, + exists, + logs, + errored, + warned + ): + self.id = instance_id + self.creator_identifier = creator_identifier + self.family = family + self.name = name + self.label = label + self.exists = exists + self.logs = logs + self.errored = errored + self.warned = warned + + def __eq__(self, other): + for attr in self._attrs: + if getattr(self, attr) != getattr(other, attr): + return False + return True + + def __ne__(self, other): + return not self.__eq__(other) + + def __gt__(self, other): + for attr in self._attrs: + self_value = getattr(self, attr) + other_value = getattr(other, attr) + if self_value == other_value: + continue + values = [self_value, other_value] + values.sort() + return values[0] == other_value + return None + + def __lt__(self, other): + for attr in self._attrs: + self_value = getattr(self, attr) + other_value = getattr(other, attr) + if self_value == other_value: + continue + if self_value is None: + return False + if other_value is None: + return True + values = [self_value, other_value] + values.sort() + return values[0] == self_value + return None + + def __ge__(self, other): + if self == other: + return True + return self.__gt__(other) + + def __le__(self, other): + if self == other: + return True + return self.__lt__(other) + + @classmethod + def from_report(cls, instance_id, instance_data, logs): + errored, warned = cls.extract_basic_log_info(logs) + + return cls( + instance_id, + instance_data["creator_identifier"], + instance_data["family"], + instance_data["name"], + instance_data["label"], + instance_data["exists"], + logs, + errored, + warned, + ) + + @classmethod + def create_context_item(cls, context_label, logs): + errored, warned = cls.extract_basic_log_info(logs) + return cls( + CONTEXT_ID, + None, + "", + CONTEXT_LABEL, + context_label, + True, + logs, + errored, + warned + ) + + @staticmethod + def extract_basic_log_info(logs): + warned = False + errored = False + for log in logs: + if log["type"] == "error": + errored = True + elif log["type"] == "record": + level_no = log["levelno"] + if level_no and level_no >= logging.WARNING: + warned = True + + if warned and errored: + break + return errored, warned + + +class FamilyGroupLabel(QtWidgets.QWidget): + def __init__(self, family, parent): + super(FamilyGroupLabel, self).__init__(parent) + + self.setLayoutDirection(QtCore.Qt.LeftToRight) + + label_widget = QtWidgets.QLabel(family, self) + + line_widget = QtWidgets.QWidget(self) + line_widget.setObjectName("Separator") + line_widget.setMinimumHeight(2) + line_widget.setMaximumHeight(2) + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.setAlignment(QtCore.Qt.AlignVCenter) + main_layout.setSpacing(10) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(label_widget, 0) + main_layout.addWidget(line_widget, 1) + + +class PublishInstanceCardWidget(BaseClickableFrame): + selection_requested = QtCore.Signal(str) + + _warning_pix = None + _error_pix = None + _success_pix = None + _in_progress_pix = None + + def __init__(self, instance, icon, publish_finished, parent): + super(PublishInstanceCardWidget, self).__init__(parent) + + self.setObjectName("CardViewWidget") + + icon_widget = IconValuePixmapLabel(icon, self) + icon_widget.setObjectName("FamilyIconLabel") + + label_widget = QtWidgets.QLabel(instance.label, self) + + if instance.errored: + state_pix = self.get_error_pix() + elif instance.warned: + state_pix = self.get_warning_pix() + elif publish_finished: + state_pix = self.get_success_pix() + else: + state_pix = self.get_in_progress_pix() + + state_label = IconValuePixmapLabel(state_pix, self) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(10, 7, 10, 7) + layout.addWidget(icon_widget, 0) + layout.addWidget(label_widget, 1) + layout.addWidget(state_label, 0) + + # Change direction -> parent is scroll area where scrolls are on + # left side + self.setLayoutDirection(QtCore.Qt.LeftToRight) + + self._id = instance.id + + self._selected = False + + self._update_style_state() + + @classmethod + def _prepare_pixes(cls): + publisher_colors = get_objected_colors("publisher") + cls._warning_pix = paint_image_with_color( + get_image("warning"), + publisher_colors["warning"].get_qcolor() + ) + cls._error_pix = paint_image_with_color( + get_image("error"), + publisher_colors["error"].get_qcolor() + ) + cls._success_pix = paint_image_with_color( + get_image("success"), + publisher_colors["success"].get_qcolor() + ) + cls._in_progress_pix = paint_image_with_color( + get_image("success"), + publisher_colors["progress"].get_qcolor() + ) + + @classmethod + def get_warning_pix(cls): + if cls._warning_pix is None: + cls._prepare_pixes() + return cls._warning_pix + + @classmethod + def get_error_pix(cls): + if cls._error_pix is None: + cls._prepare_pixes() + return cls._error_pix + + @classmethod + def get_success_pix(cls): + if cls._success_pix is None: + cls._prepare_pixes() + return cls._success_pix + + @classmethod + def get_in_progress_pix(cls): + if cls._in_progress_pix is None: + cls._prepare_pixes() + return cls._in_progress_pix + + @property + def id(self): + """Id of card. + + Returns: + str: Id of item. + """ + + return self._id + + @property + def is_selected(self): + """Is card selected. + + Returns: + bool: Item widget is marked as selected. + """ + + return self._selected + + def set_selected(self, selected): + """Set card as selected. + + Args: + selected (bool): Item should be marked as selected. + """ + + if selected == self._selected: + return + self._selected = selected + self._update_style_state() + + def _update_style_state(self): + state = "" + if self._selected: + state = "selected" + + self.setProperty("state", state) + self.style().polish(self) + + def _mouse_release_callback(self): + """Trigger selected signal.""" + + self.selection_requested.emit(self.id) + + +class PublishInstancesViewWidget(QtWidgets.QWidget): + # Sane minimum width of instance cards - size calulated using font metrics + _min_width_measure_string = 24 * "O" + selection_changed = QtCore.Signal() + + def __init__(self, controller, parent): + super(PublishInstancesViewWidget, self).__init__(parent) + + scroll_area = VerticalScrollArea(self) + scroll_area.setWidgetResizable(True) + scroll_area.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + scroll_area.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) + scrollbar_bg = scroll_area.verticalScrollBar().parent() + if scrollbar_bg: + scrollbar_bg.setAttribute(QtCore.Qt.WA_TranslucentBackground) + scroll_area.setViewportMargins(0, 0, 0, 0) + + instance_view = QtWidgets.QWidget(scroll_area) + + scroll_area.setWidget(instance_view) + + instance_layout = QtWidgets.QVBoxLayout(instance_view) + instance_layout.setContentsMargins(0, 0, 0, 0) + instance_layout.addStretch(1) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(scroll_area, 1) + + self._controller = controller + self._scroll_area = scroll_area + self._instance_view = instance_view + self._instance_layout = instance_layout + + self._context_widget = None + + self._widgets_by_instance_id = {} + self._group_widgets = [] + self._ordered_widgets = [] + + self._explicitly_selected_instance_ids = [] + + self.setSizePolicy( + QtWidgets.QSizePolicy.Minimum, + self.sizePolicy().verticalPolicy() + ) + + def sizeHint(self): + """Modify sizeHint based on visibility of scroll bars.""" + # Calculate width hint by content widget and vertical scroll bar + scroll_bar = self._scroll_area.verticalScrollBar() + view_size = self._instance_view.sizeHint().width() + fm = self._instance_view.fontMetrics() + width = ( + max(view_size, fm.width(self._min_width_measure_string)) + + scroll_bar.sizeHint().width() + ) + + result = super(PublishInstancesViewWidget, self).sizeHint() + result.setWidth(width) + return result + + def _get_selected_widgets(self): + return [ + widget + for widget in self._ordered_widgets + if widget.is_selected + ] + + def get_selected_instance_ids(self): + return [ + widget.id + for widget in self._get_selected_widgets() + ] + + def clear(self): + """Remove actions from widget.""" + while self._instance_layout.count(): + item = self._instance_layout.takeAt(0) + widget = item.widget() + if widget: + widget.setVisible(False) + widget.deleteLater() + self._ordered_widgets = [] + self._group_widgets = [] + self._widgets_by_instance_id = {} + + def update_instances(self, instance_items): + self.clear() + identifiers = { + instance_item.creator_identifier + for instance_item in instance_items + } + identifier_icons = { + identifier: self._controller.get_creator_icon(identifier) + for identifier in identifiers + } + + widgets = [] + group_widgets = [] + + publish_finished = ( + self._controller.publish_has_crashed + or self._controller.publish_has_validation_errors + or self._controller.publish_has_finished + ) + instances_by_family = collections.defaultdict(list) + for instance_item in instance_items: + if not instance_item.exists: + continue + instances_by_family[instance_item.family].append(instance_item) + + sorted_by_family = sorted( + instances_by_family.items(), key=lambda i: i[0] + ) + for family, instance_items in sorted_by_family: + # Only instance without family is context + if family: + group_widget = FamilyGroupLabel(family, self._instance_view) + self._instance_layout.addWidget(group_widget, 0) + group_widgets.append(group_widget) + + sorted_items = sorted(instance_items, key=lambda i: i.label) + for instance_item in sorted_items: + icon = identifier_icons[instance_item.creator_identifier] + + widget = PublishInstanceCardWidget( + instance_item, icon, publish_finished, self._instance_view + ) + widget.selection_requested.connect(self._on_selection_request) + self._instance_layout.addWidget(widget, 0) + + widgets.append(widget) + self._widgets_by_instance_id[widget.id] = widget + self._instance_layout.addStretch(1) + self._ordered_widgets = widgets + self._group_widgets = group_widgets + + def _on_selection_request(self, instance_id): + instance_widget = self._widgets_by_instance_id[instance_id] + selected_widgets = self._get_selected_widgets() + if instance_widget in selected_widgets: + instance_widget.set_selected(False) + else: + instance_widget.set_selected(True) + for widget in selected_widgets: + widget.set_selected(False) + self.selection_changed.emit() + + +class LogIconFrame(QtWidgets.QFrame): + """Draw log item icon next to message. + + Todos: + Paint event could be slow, maybe we could cache the image into pixmaps + so each item does not have to redraw it again. + """ + + info_color = QtGui.QColor("#ffffff") + error_color = QtGui.QColor("#ff4a4a") + level_to_color = dict(( + (10, QtGui.QColor("#ff66e8")), + (20, QtGui.QColor("#66abff")), + (30, QtGui.QColor("#ffba66")), + (40, QtGui.QColor("#ff4d58")), + (50, QtGui.QColor("#ff4f75")), + )) + _error_pix = None + _validation_error_pix = None + + def __init__(self, parent, log_type, log_level, is_validation_error): + super(LogIconFrame, self).__init__(parent) + + self.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + self._is_record = log_type == "record" + self._is_error = log_type == "error" + self._is_validation_error = bool(is_validation_error) + self._log_color = self.level_to_color.get(log_level) + + @classmethod + def get_validation_error_icon(cls): + if cls._validation_error_pix is None: + cls._validation_error_pix = get_pixmap("warning") + return cls._validation_error_pix + + @classmethod + def get_error_icon(cls): + if cls._error_pix is None: + cls._error_pix = get_pixmap("error") + return cls._error_pix + + def minimumSizeHint(self): + fm = self.fontMetrics() + size = fm.height() + return QtCore.QSize(size, size) + + def paintEvent(self, event): + painter = QtGui.QPainter(self) + painter.setRenderHints( + QtGui.QPainter.Antialiasing + | QtGui.QPainter.SmoothPixmapTransform + ) + painter.setPen(QtCore.Qt.NoPen) + rect = self.rect() + new_size = min(rect.width(), rect.height()) + new_rect = QtCore.QRect(1, 1, new_size - 2, new_size - 2) + if self._is_error: + if self._is_validation_error: + error_icon = self.get_validation_error_icon() + else: + error_icon = self.get_error_icon() + scaled_error_icon = error_icon.scaled( + new_rect.size(), + QtCore.Qt.KeepAspectRatio, + QtCore.Qt.SmoothTransformation + ) + painter.drawPixmap(new_rect, scaled_error_icon) + + else: + if self._is_record: + color = self._log_color + else: + color = QtGui.QColor(255, 255, 255) + painter.setBrush(color) + painter.drawEllipse(new_rect) + painter.end() + + +class LogItemWidget(QtWidgets.QWidget): + log_level_to_flag = { + 10: LOG_DEBUG_VISIBLE, + 20: LOG_INFO_VISIBLE, + 30: LOG_WARNING_VISIBLE, + 40: LOG_ERROR_VISIBLE, + 50: LOG_CRITICAL_VISIBLE, + } + + def __init__(self, log, parent): + super(LogItemWidget, self).__init__(parent) + + type_flag, level_n = self._get_log_info(log) + icon_label = LogIconFrame( + self, log["type"], level_n, log.get("is_validation_error")) + message_label = QtWidgets.QLabel(log["msg"].rstrip(), self) + message_label.setObjectName("PublishLogMessage") + message_label.setTextInteractionFlags( + QtCore.Qt.TextBrowserInteraction) + message_label.setCursor(QtGui.QCursor(QtCore.Qt.IBeamCursor)) + message_label.setWordWrap(True) + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.setSpacing(8) + main_layout.addWidget(icon_label, 0) + main_layout.addWidget(message_label, 1) + + self._type_flag = type_flag + self._plugin_id = log["plugin_id"] + self._log_type_filtered = False + self._plugin_filtered = False + + @property + def type_flag(self): + return self._type_flag + + @property + def plugin_id(self): + return self._plugin_id + + def _get_log_info(self, log): + log_type = log["type"] + if log_type == "error": + return ERROR_VISIBLE, None + + if log_type != "record": + return INFO_VISIBLE, None + + level_n = log["levelno"] + if level_n < 10: + level_n = 10 + elif level_n % 10 != 0: + level_n -= (level_n % 10) + 10 + + flag = self.log_level_to_flag.get(level_n, LOG_CRITICAL_VISIBLE) + return flag, level_n + + def _update_visibility(self): + self.setVisible( + not self._log_type_filtered + and not self._plugin_filtered + ) + + def set_log_type_filtered(self, filtered): + if filtered is self._log_type_filtered: + return + self._log_type_filtered = filtered + self._update_visibility() + + def set_plugin_filtered(self, filtered): + if filtered is self._plugin_filtered: + return + self._plugin_filtered = filtered + self._update_visibility() + + +class LogsWithIconsView(QtWidgets.QWidget): + """Show logs in a grid with 2 columns. + + First column is for icon second is for message. + + Todos: + Add filtering by type (exception, debug, info, etc.). + """ + + def __init__(self, logs, parent): + super(LogsWithIconsView, self).__init__(parent) + self.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + logs_layout = QtWidgets.QVBoxLayout(self) + logs_layout.setContentsMargins(0, 0, 0, 0) + logs_layout.setSpacing(4) + + widgets_by_flag = collections.defaultdict(list) + widgets_by_plugins_id = collections.defaultdict(list) + + for log in logs: + widget = LogItemWidget(log, self) + widgets_by_flag[widget.type_flag].append(widget) + widgets_by_plugins_id[widget.plugin_id].append(widget) + logs_layout.addWidget(widget, 0) + + self._widgets_by_flag = widgets_by_flag + self._widgets_by_plugins_id = widgets_by_plugins_id + + self._visibility_by_flags = { + LOG_DEBUG_VISIBLE: True, + LOG_INFO_VISIBLE: True, + LOG_WARNING_VISIBLE: True, + LOG_ERROR_VISIBLE: True, + LOG_CRITICAL_VISIBLE: True, + ERROR_VISIBLE: True, + INFO_VISIBLE: True, + } + self._flags_filter = sum(self._visibility_by_flags.keys()) + self._plugin_ids_filter = None + + def _update_flags_filtering(self): + for flag in ( + LOG_DEBUG_VISIBLE, + LOG_INFO_VISIBLE, + LOG_WARNING_VISIBLE, + LOG_ERROR_VISIBLE, + LOG_CRITICAL_VISIBLE, + ERROR_VISIBLE, + INFO_VISIBLE, + ): + visible = (self._flags_filter & flag) != 0 + if visible is not self._visibility_by_flags[flag]: + self._visibility_by_flags[flag] = visible + for widget in self._widgets_by_flag[flag]: + widget.set_log_type_filtered(not visible) + + def _update_plugin_filtering(self): + if self._plugin_ids_filter is None: + for widgets in self._widgets_by_plugins_id.values(): + for widget in widgets: + widget.set_plugin_filtered(False) + + else: + for plugin_id, widgets in self._widgets_by_plugins_id.items(): + filtered = plugin_id not in self._plugin_ids_filter + for widget in widgets: + widget.set_plugin_filtered(filtered) + + def set_log_filters(self, visibility_filter, plugin_ids): + if self._flags_filter != visibility_filter: + self._flags_filter = visibility_filter + self._update_flags_filtering() + + if self._plugin_ids_filter != plugin_ids: + if plugin_ids is not None: + plugin_ids = set(plugin_ids) + self._plugin_ids_filter = plugin_ids + self._update_plugin_filtering() + + +class InstanceLogsWidget(QtWidgets.QWidget): + """Widget showing logs of one publish instance. + + Args: + instance (_InstanceItem): Item of instance used as data source. + parent (QtWidgets.QWidget): Parent widget. + """ + + def __init__(self, instance, parent): + super(InstanceLogsWidget, self).__init__(parent) + + self.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + label_widget = QtWidgets.QLabel(instance.label, self) + label_widget.setObjectName("PublishInstanceLogsLabel") + logs_grid = LogsWithIconsView(instance.logs, self) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(label_widget, 0) + layout.addWidget(logs_grid, 0) + + self._logs_grid = logs_grid + + def set_log_filters(self, visibility_filter, plugin_ids): + """Change logs filter. + + Args: + visibility_filter (int): Number contained of flags for each log + type and level. + plugin_ids (Iterable[str]): Plugin ids to which are logs filtered. + """ + + self._logs_grid.set_log_filters(visibility_filter, plugin_ids) + + +class InstancesLogsView(QtWidgets.QFrame): + """Publish instances logs view widget.""" + + def __init__(self, parent): + super(InstancesLogsView, self).__init__(parent) + self.setObjectName("InstancesLogsView") + + scroll_area = QtWidgets.QScrollArea(self) + scroll_area.setWidgetResizable(True) + scroll_area.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + scroll_area.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) + scroll_area.setAttribute(QtCore.Qt.WA_TranslucentBackground) + scrollbar_bg = scroll_area.verticalScrollBar().parent() + if scrollbar_bg: + scrollbar_bg.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + content_wrap_widget = QtWidgets.QWidget(scroll_area) + content_wrap_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + content_widget = QtWidgets.QWidget(content_wrap_widget) + content_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + content_layout = QtWidgets.QVBoxLayout(content_widget) + content_layout.setSpacing(15) + + scroll_area.setWidget(content_wrap_widget) + + content_wrap_layout = QtWidgets.QVBoxLayout(content_wrap_widget) + content_wrap_layout.setContentsMargins(0, 0, 0, 0) + content_wrap_layout.addWidget(content_widget, 0) + content_wrap_layout.addStretch(1) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(scroll_area, 1) + + self._visible_filters = ( + LOG_INFO_VISIBLE + | LOG_WARNING_VISIBLE + | LOG_ERROR_VISIBLE + | LOG_CRITICAL_VISIBLE + | ERROR_VISIBLE + | INFO_VISIBLE + ) + + self._content_widget = content_widget + self._content_layout = content_layout + + self._instances_order = [] + self._instances_by_id = {} + self._views_by_instance_id = {} + self._is_showed = False + self._clear_needed = False + self._update_needed = False + self._instance_ids_filter = [] + self._plugin_ids_filter = None + + def showEvent(self, event): + super(InstancesLogsView, self).showEvent(event) + self._is_showed = True + self._update_instances() + + def hideEvent(self, event): + super(InstancesLogsView, self).hideEvent(event) + self._is_showed = False + + def closeEvent(self, event): + super(InstancesLogsView, self).closeEvent(event) + self._is_showed = False + + def _update_instances(self): + if not self._is_showed: + return + + if self._clear_needed: + self._clear_widgets() + self._clear_needed = False + + if not self._update_needed: + return + self._update_needed = False + + instance_ids = self._instance_ids_filter + to_hide = set() + if not instance_ids: + instance_ids = self._instances_by_id + else: + to_hide = set(self._instances_by_id) - set(instance_ids) + + for instance_id in instance_ids: + widget = self._views_by_instance_id.get(instance_id) + if widget is None: + instance = self._instances_by_id[instance_id] + widget = InstanceLogsWidget(instance, self._content_widget) + self._views_by_instance_id[instance_id] = widget + self._content_layout.addWidget(widget, 0) + + widget.setVisible(True) + widget.set_log_filters( + self._visible_filters, self._plugin_ids_filter + ) + + for instance_id in to_hide: + widget = self._views_by_instance_id.get(instance_id) + if widget is not None: + widget.setVisible(False) + + def _clear_widgets(self): + """Remove all widgets from layout and from cache.""" + + while self._content_layout.count(): + item = self._content_layout.takeAt(0) + widget = item.widget() + if widget: + widget.setVisible(False) + widget.deleteLater() + self._views_by_instance_id = {} + + def update_instances(self, instances): + """Update publish instance from report. + + Args: + instances (list[_InstanceItem]): Instance data from report. + """ + + self._instances_order = [ + instance.id for instance in instances + ] + self._instances_by_id = { + instance.id: instance + for instance in instances + } + self._instance_ids_filter = [] + self._plugin_ids_filter = None + self._clear_needed = True + self._update_needed = True + self._update_instances() + + def set_instances_filter(self, instance_ids=None): + """Set instance filter. + + Args: + instance_ids (Optional[list[str]]): List of instances to keep + visible. Pass empty list to hide all items. + """ + + self._instance_ids_filter = instance_ids + self._update_needed = True + self._update_instances() + + def set_plugins_filter(self, plugin_ids=None): + if self._plugin_ids_filter == plugin_ids: + return + self._plugin_ids_filter = plugin_ids + self._update_needed = True + self._update_instances() + + +class CrashWidget(QtWidgets.QWidget): + """Widget shown when publishing crashes. + + Contains only minimal information for artist with easy access to report + actions. + """ + + def __init__(self, controller, parent): + super(CrashWidget, self).__init__(parent) + + main_label = QtWidgets.QLabel("This is not your fault", self) + main_label.setAlignment(QtCore.Qt.AlignCenter) + main_label.setObjectName("PublishCrashMainLabel") + + report_label = QtWidgets.QLabel( + ( + "Please report the error to your pipeline support" + " using one of the options below." + ), + self + ) + report_label.setAlignment(QtCore.Qt.AlignCenter) + report_label.setWordWrap(True) + report_label.setObjectName("PublishCrashReportLabel") + + btns_widget = QtWidgets.QWidget(self) + copy_clipboard_btn = QtWidgets.QPushButton( + "Copy to clipboard", btns_widget) + save_to_disk_btn = QtWidgets.QPushButton( + "Save to disk", btns_widget) + + btns_layout = QtWidgets.QHBoxLayout(btns_widget) + btns_layout.addStretch(1) + btns_layout.addWidget(copy_clipboard_btn, 0) + btns_layout.addSpacing(20) + btns_layout.addWidget(save_to_disk_btn, 0) + btns_layout.addStretch(1) + + layout = QtWidgets.QVBoxLayout(self) + layout.addStretch(1) + layout.addWidget(main_label, 0) + layout.addSpacing(20) + layout.addWidget(report_label, 0) + layout.addSpacing(20) + layout.addWidget(btns_widget, 0) + layout.addStretch(2) + + copy_clipboard_btn.clicked.connect(self._on_copy_to_clipboard) + save_to_disk_btn.clicked.connect(self._on_save_to_disk_click) + + self._controller = controller + + def _on_copy_to_clipboard(self): + self._controller.event_system.emit( + "copy_report.request", {}, "report_page") + + def _on_save_to_disk_click(self): + self._controller.event_system.emit( + "export_report.request", {}, "report_page") + + +class ErrorDetailsWidget(QtWidgets.QWidget): + def __init__(self, parent): + super(ErrorDetailsWidget, self).__init__(parent) + + inputs_widget = QtWidgets.QWidget(self) + # Error 'Description' input + error_description_input = ExpandingTextEdit(inputs_widget) + error_description_input.setObjectName("InfoText") + error_description_input.setTextInteractionFlags( + QtCore.Qt.TextBrowserInteraction + ) + + # Error 'Details' widget -> Collapsible + error_details_widget = QtWidgets.QWidget(inputs_widget) + + error_details_top = ClickableFrame(error_details_widget) + + error_details_expand_btn = ClassicExpandBtn(error_details_top) + error_details_expand_label = QtWidgets.QLabel( + "Details", error_details_top) + + line_widget = SeparatorWidget(1, parent=error_details_top) + + error_details_top_l = QtWidgets.QHBoxLayout(error_details_top) + error_details_top_l.setContentsMargins(0, 0, 10, 0) + error_details_top_l.addWidget(error_details_expand_btn, 0) + error_details_top_l.addWidget(error_details_expand_label, 0) + error_details_top_l.addWidget(line_widget, 1) + + error_details_input = ExpandingTextEdit(error_details_widget) + error_details_input.setObjectName("InfoText") + error_details_input.setTextInteractionFlags( + QtCore.Qt.TextBrowserInteraction + ) + error_details_input.setVisible(not error_details_expand_btn.collapsed) + + error_details_layout = QtWidgets.QVBoxLayout(error_details_widget) + error_details_layout.setContentsMargins(0, 0, 0, 0) + error_details_layout.addWidget(error_details_top, 0) + error_details_layout.addWidget(error_details_input, 0) + error_details_layout.addStretch(1) + + # Description and Details layout + inputs_layout = QtWidgets.QVBoxLayout(inputs_widget) + inputs_layout.setContentsMargins(0, 0, 0, 0) + inputs_layout.setSpacing(10) + inputs_layout.addWidget(error_description_input, 0) + inputs_layout.addWidget(error_details_widget, 1) + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(inputs_widget, 1) + + error_details_top.clicked.connect(self._on_detail_toggle) + + self._error_details_widget = error_details_widget + self._error_description_input = error_description_input + self._error_details_expand_btn = error_details_expand_btn + self._error_details_input = error_details_input + + def _on_detail_toggle(self): + self._error_details_expand_btn.set_collapsed() + self._error_details_input.setVisible( + not self._error_details_expand_btn.collapsed) + + def set_error_item(self, error_item): + detail = "" + description = "" + if error_item: + description = error_item.description or description + detail = error_item.detail or detail + + if commonmark: + self._error_description_input.setHtml( + commonmark.commonmark(description) + ) + self._error_details_input.setHtml( + commonmark.commonmark(detail) + ) + + elif hasattr(self._error_details_input, "setMarkdown"): + self._error_description_input.setMarkdown(description) + self._error_details_input.setMarkdown(detail) + + else: + self._error_description_input.setText(description) + self._error_details_input.setText(detail) + + self._error_details_widget.setVisible(bool(detail)) + + +class ReportsWidget(QtWidgets.QWidget): + """ + # Crash layout + β”Œβ”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚Views β”‚ Logs β”‚ Details β”‚ + β”‚ β”‚ β”‚ β”‚ + β”‚ β”‚ β”‚ β”‚ + β””β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + # Success layout + β”Œβ”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚View β”‚ Logs β”‚ + β”‚ β”‚ β”‚ + β”‚ β”‚ β”‚ + β””β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + # Validation errors layout + β”Œβ”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚Views β”‚ Actions β”‚ β”‚ + β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ Details β”‚ + β”‚ β”‚ Logs β”‚ β”‚ + β”‚ β”‚ β”‚ β”‚ + β””β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + """ + + def __init__(self, controller, parent): + super(ReportsWidget, self).__init__(parent) + + # Instances view + views_widget = QtWidgets.QWidget(self) + + instances_view = PublishInstancesViewWidget(controller, views_widget) + + validation_error_view = ValidationErrorsView(views_widget) + + views_layout = QtWidgets.QStackedLayout(views_widget) + views_layout.setContentsMargins(0, 0, 0, 0) + views_layout.addWidget(instances_view) + views_layout.addWidget(validation_error_view) + + views_layout.setCurrentWidget(instances_view) + + # Error description with actions and optional detail + details_widget = QtWidgets.QFrame(self) + details_widget.setObjectName("PublishInstancesDetails") + + # Actions widget + actions_widget = ValidateActionsWidget(controller, details_widget) + + pages_widget = QtWidgets.QWidget(details_widget) + + # Logs view + logs_view = InstancesLogsView(pages_widget) + + # Validation details + # Description and details inputs are in scroll + # - single scroll for both inputs, they are forced to not use theirs + detail_inputs_spacer = QtWidgets.QWidget(pages_widget) + detail_inputs_spacer.setMinimumWidth(30) + detail_inputs_spacer.setMaximumWidth(30) + + detail_input_scroll = QtWidgets.QScrollArea(pages_widget) + + detail_inputs_widget = ErrorDetailsWidget(detail_input_scroll) + detail_inputs_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + detail_input_scroll.setWidget(detail_inputs_widget) + detail_input_scroll.setWidgetResizable(True) + detail_input_scroll.setViewportMargins(0, 0, 0, 0) + + # Crash information + crash_widget = CrashWidget(controller, details_widget) + + # Layout pages + pages_layout = QtWidgets.QHBoxLayout(pages_widget) + pages_layout.setContentsMargins(0, 0, 0, 0) + pages_layout.addWidget(logs_view, 1) + pages_layout.addWidget(detail_inputs_spacer, 0) + pages_layout.addWidget(detail_input_scroll, 1) + pages_layout.addWidget(crash_widget, 1) + + details_layout = QtWidgets.QVBoxLayout(details_widget) + margins = details_layout.contentsMargins() + margins.setTop(margins.top() * 2) + margins.setBottom(margins.bottom() * 2) + details_layout.setContentsMargins(margins) + details_layout.setSpacing(margins.top()) + details_layout.addWidget(actions_widget, 0) + details_layout.addWidget(pages_widget, 1) + + content_layout = QtWidgets.QHBoxLayout(self) + content_layout.setContentsMargins(0, 0, 0, 0) + content_layout.addWidget(views_widget, 0) + content_layout.addWidget(details_widget, 1) + + instances_view.selection_changed.connect(self._on_instance_selection) + validation_error_view.selection_changed.connect( + self._on_error_selection) + + self._views_layout = views_layout + self._instances_view = instances_view + self._validation_error_view = validation_error_view + + self._actions_widget = actions_widget + self._detail_inputs_widget = detail_inputs_widget + self._logs_view = logs_view + self._detail_inputs_spacer = detail_inputs_spacer + self._detail_input_scroll = detail_input_scroll + self._crash_widget = crash_widget + + self._controller = controller + + self._validation_errors_by_id = {} + + def _get_instance_items(self): + report = self._controller.get_publish_report() + context_label = report["context"]["label"] or CONTEXT_LABEL + instances_by_id = report["instances"] + plugins_info = report["plugins_data"] + logs_by_instance_id = collections.defaultdict(list) + for plugin_info in plugins_info: + plugin_id = plugin_info["id"] + for instance_info in plugin_info["instances_data"]: + instance_id = instance_info["id"] or CONTEXT_ID + for log in instance_info["logs"]: + log["plugin_id"] = plugin_id + logs_by_instance_id[instance_id].extend(instance_info["logs"]) + + context_item = _InstanceItem.create_context_item( + context_label, logs_by_instance_id[CONTEXT_ID]) + instance_items = [ + _InstanceItem.from_report( + instance_id, instance, logs_by_instance_id[instance_id] + ) + for instance_id, instance in instances_by_id.items() + if instance["exists"] + ] + instance_items.sort() + instance_items.insert(0, context_item) + return instance_items + + def update_data(self): + view = self._instances_view + validation_error_mode = False + if ( + not self._controller.publish_has_crashed + and self._controller.publish_has_validation_errors + ): + view = self._validation_error_view + validation_error_mode = True + + self._actions_widget.set_visible_mode(validation_error_mode) + self._detail_inputs_spacer.setVisible(validation_error_mode) + self._detail_input_scroll.setVisible(validation_error_mode) + self._views_layout.setCurrentWidget(view) + + self._crash_widget.setVisible(self._controller.publish_has_crashed) + self._logs_view.setVisible(not self._controller.publish_has_crashed) + + # Instance view & logs update + instance_items = self._get_instance_items() + self._instances_view.update_instances(instance_items) + self._logs_view.update_instances(instance_items) + + # Validation errors + validation_errors = self._controller.get_validation_errors() + grouped_error_items = validation_errors.group_items_by_title() + + validation_errors_by_id = { + title_item["id"]: title_item + for title_item in grouped_error_items + } + + self._validation_errors_by_id = validation_errors_by_id + self._validation_error_view.set_errors(grouped_error_items) + + def _on_instance_selection(self): + instance_ids = self._instances_view.get_selected_instance_ids() + self._logs_view.set_instances_filter(instance_ids) + + def _on_error_selection(self): + title_id, instance_ids = ( + self._validation_error_view.get_selected_items()) + error_info = self._validation_errors_by_id.get(title_id) + if error_info is None: + self._actions_widget.set_error_info(None) + self._detail_inputs_widget.set_error_item(None) + return + + self._logs_view.set_instances_filter(instance_ids) + self._logs_view.set_plugins_filter([error_info["plugin_id"]]) + + match_error_item = None + for error_item in error_info["error_items"]: + instance_id = error_item.instance_id or CONTEXT_ID + if instance_id in instance_ids: + match_error_item = error_item + break + + self._actions_widget.set_error_info(error_info) + self._detail_inputs_widget.set_error_item(match_error_item) + + +class ReportPageWidget(QtWidgets.QFrame): + """Widgets showing report for artis. + + There are 5 possible states: + 1. Publishing did not start yet. > Only label. + 2. Publishing is paused. ┐ + 3. Publishing successfully finished. β”‚> Instances with logs. + 4. Publishing crashed. β”˜ + 5. Crashed because of validation error. > Errors with logs. + + This widget is shown if validation errors happened during validation part. + + Shows validation error titles with instances on which they happened + and validation error detail with possible actions (repair). + """ + + def __init__(self, controller, parent): + super(ReportPageWidget, self).__init__(parent) + + header_label = QtWidgets.QLabel(self) + header_label.setAlignment(QtCore.Qt.AlignCenter) + header_label.setObjectName("PublishReportHeader") + + publish_instances_widget = ReportsWidget(controller, self) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(header_label, 0) + layout.addWidget(publish_instances_widget, 0) + + controller.event_system.add_callback( + "publish.process.started", self._on_publish_start + ) + controller.event_system.add_callback( + "publish.reset.finished", self._on_publish_reset + ) + controller.event_system.add_callback( + "publish.process.stopped", self._on_publish_stop + ) + + self._header_label = header_label + self._publish_instances_widget = publish_instances_widget + + self._controller = controller + + def _update_label(self): + if not self._controller.publish_has_started: + # This probably never happen when this widget is visible + header_label = "Nothing to report until you run publish" + elif self._controller.publish_has_crashed: + header_label = "Publish error report" + elif self._controller.publish_has_validation_errors: + header_label = "Publish validation report" + elif self._controller.publish_has_finished: + header_label = "Publish success report" + else: + header_label = "Publish report" + self._header_label.setText(header_label) + + def _update_state(self): + self._update_label() + publish_started = self._controller.publish_has_started + self._publish_instances_widget.setVisible(publish_started) + if publish_started: + self._publish_instances_widget.update_data() + + self.updateGeometry() + + def _on_publish_start(self): + self._update_state() + + def _on_publish_reset(self): + self._update_state() + + def _on_publish_stop(self): + self._update_state() diff --git a/openpype/tools/publisher/widgets/thumbnail_widget.py b/openpype/tools/publisher/widgets/thumbnail_widget.py index e234f4cdc1..b17ca0adc8 100644 --- a/openpype/tools/publisher/widgets/thumbnail_widget.py +++ b/openpype/tools/publisher/widgets/thumbnail_widget.py @@ -75,6 +75,7 @@ class ThumbnailPainterWidget(QtWidgets.QWidget): painter = QtGui.QPainter() painter.begin(self) + painter.setRenderHint(QtGui.QPainter.Antialiasing) painter.drawPixmap(0, 0, self._cached_pix) painter.end() @@ -183,6 +184,18 @@ class ThumbnailPainterWidget(QtWidgets.QWidget): backgrounded_images.append(new_pix) return backgrounded_images + def _paint_dash_line(self, painter, rect): + pen = QtGui.QPen() + pen.setWidth(1) + pen.setBrush(QtCore.Qt.darkGray) + pen.setStyle(QtCore.Qt.DashLine) + + new_rect = rect.adjusted(1, 1, -1, -1) + painter.setPen(pen) + painter.setBrush(QtCore.Qt.transparent) + # painter.drawRect(rect) + painter.drawRect(new_rect) + def _cache_pix(self): rect = self.rect() rect_width = rect.width() @@ -264,13 +277,7 @@ class ThumbnailPainterWidget(QtWidgets.QWidget): # Draw drop enabled dashes if used_default_pix: - pen = QtGui.QPen() - pen.setWidth(1) - pen.setBrush(QtCore.Qt.darkGray) - pen.setStyle(QtCore.Qt.DashLine) - final_painter.setPen(pen) - final_painter.setBrush(QtCore.Qt.transparent) - final_painter.drawRect(rect) + self._paint_dash_line(final_painter, rect) final_painter.end() diff --git a/openpype/tools/publisher/widgets/validations_widget.py b/openpype/tools/publisher/widgets/validations_widget.py deleted file mode 100644 index 0abe85c0b8..0000000000 --- a/openpype/tools/publisher/widgets/validations_widget.py +++ /dev/null @@ -1,715 +0,0 @@ -# -*- coding: utf-8 -*- -try: - import commonmark -except Exception: - commonmark = None - -from qtpy import QtWidgets, QtCore, QtGui - -from openpype.tools.utils import BaseClickableFrame, ClickableFrame -from .widgets import ( - IconValuePixmapLabel -) -from ..constants import ( - INSTANCE_ID_ROLE -) - - -class ValidationErrorInstanceList(QtWidgets.QListView): - """List of publish instances that caused a validation error. - - Instances are collected per plugin's validation error title. - """ - def __init__(self, *args, **kwargs): - super(ValidationErrorInstanceList, self).__init__(*args, **kwargs) - - self.setObjectName("ValidationErrorInstanceList") - - self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) - - def minimumSizeHint(self): - return self.sizeHint() - - def sizeHint(self): - result = super(ValidationErrorInstanceList, self).sizeHint() - row_count = self.model().rowCount() - height = 0 - if row_count > 0: - height = self.sizeHintForRow(0) * row_count - result.setHeight(height) - return result - - -class ValidationErrorTitleWidget(QtWidgets.QWidget): - """Title of validation error. - - Widget is used as radio button so requires clickable functionality and - changing style on selection/deselection. - - Has toggle button to show/hide instances on which validation error happened - if there is a list (Valdation error may happen on context). - """ - - selected = QtCore.Signal(int) - instance_changed = QtCore.Signal(int) - - def __init__(self, index, error_info, parent): - super(ValidationErrorTitleWidget, self).__init__(parent) - - self._index = index - self._error_info = error_info - self._selected = False - - title_frame = ClickableFrame(self) - title_frame.setObjectName("ValidationErrorTitleFrame") - - toggle_instance_btn = QtWidgets.QToolButton(title_frame) - toggle_instance_btn.setObjectName("ArrowBtn") - toggle_instance_btn.setArrowType(QtCore.Qt.RightArrow) - toggle_instance_btn.setMaximumWidth(14) - - label_widget = QtWidgets.QLabel(error_info["title"], title_frame) - - title_frame_layout = QtWidgets.QHBoxLayout(title_frame) - title_frame_layout.addWidget(label_widget, 1) - title_frame_layout.addWidget(toggle_instance_btn, 0) - - instances_model = QtGui.QStandardItemModel() - - help_text_by_instance_id = {} - - items = [] - context_validation = False - for error_item in error_info["error_items"]: - context_validation = error_item.context_validation - if context_validation: - toggle_instance_btn.setArrowType(QtCore.Qt.NoArrow) - description = self._prepare_description(error_item) - help_text_by_instance_id[None] = description - # Add fake item to have minimum size hint of view widget - items.append(QtGui.QStandardItem("Context")) - continue - - label = error_item.instance_label - item = QtGui.QStandardItem(label) - item.setFlags( - QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable - ) - item.setData(label, QtCore.Qt.ToolTipRole) - item.setData(error_item.instance_id, INSTANCE_ID_ROLE) - items.append(item) - description = self._prepare_description(error_item) - help_text_by_instance_id[error_item.instance_id] = description - - if items: - root_item = instances_model.invisibleRootItem() - root_item.appendRows(items) - - instances_view = ValidationErrorInstanceList(self) - instances_view.setModel(instances_model) - - self.setLayoutDirection(QtCore.Qt.LeftToRight) - - view_widget = QtWidgets.QWidget(self) - view_layout = QtWidgets.QHBoxLayout(view_widget) - view_layout.setContentsMargins(0, 0, 0, 0) - view_layout.setSpacing(0) - view_layout.addSpacing(14) - view_layout.addWidget(instances_view, 0) - - layout = QtWidgets.QVBoxLayout(self) - layout.setSpacing(0) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(title_frame, 0) - layout.addWidget(view_widget, 0) - view_widget.setVisible(False) - - if not context_validation: - toggle_instance_btn.clicked.connect(self._on_toggle_btn_click) - - title_frame.clicked.connect(self._mouse_release_callback) - instances_view.selectionModel().selectionChanged.connect( - self._on_seleciton_change - ) - - self._title_frame = title_frame - - self._toggle_instance_btn = toggle_instance_btn - - self._view_widget = view_widget - - self._instances_model = instances_model - self._instances_view = instances_view - - self._context_validation = context_validation - self._help_text_by_instance_id = help_text_by_instance_id - - self._expanded = False - - def sizeHint(self): - result = super(ValidationErrorTitleWidget, self).sizeHint() - expected_width = max( - self._view_widget.minimumSizeHint().width(), - self._view_widget.sizeHint().width() - ) - - if expected_width < 200: - expected_width = 200 - - if result.width() < expected_width: - result.setWidth(expected_width) - - return result - - def minimumSizeHint(self): - return self.sizeHint() - - def _prepare_description(self, error_item): - """Prepare description text for detail intput. - - Args: - error_item (ValidationErrorItem): Item which hold information about - validation error. - - Returns: - str: Prepared detailed description. - """ - - dsc = error_item.description - detail = error_item.detail - if detail: - dsc += "

{}".format(detail) - - description = dsc - if commonmark: - description = commonmark.commonmark(dsc) - return description - - def _mouse_release_callback(self): - """Mark this widget as selected on click.""" - - self.set_selected(True) - - def current_description_text(self): - if self._context_validation: - return self._help_text_by_instance_id[None] - index = self._instances_view.currentIndex() - # TODO make sure instance is selected - if not index.isValid(): - index = self._instances_model.index(0, 0) - - indence_id = index.data(INSTANCE_ID_ROLE) - return self._help_text_by_instance_id[indence_id] - - @property - def is_selected(self): - """Is widget marked a selected. - - Returns: - bool: Item is selected or not. - """ - - return self._selected - - @property - def index(self): - """Widget's index set by parent. - - Returns: - int: Index of widget. - """ - - return self._index - - def set_index(self, index): - """Set index of widget (called by parent). - - Args: - int: New index of widget. - """ - - self._index = index - - def _change_style_property(self, selected): - """Change style of widget based on selection.""" - - value = "1" if selected else "" - self._title_frame.setProperty("selected", value) - self._title_frame.style().polish(self._title_frame) - - def set_selected(self, selected=None): - """Change selected state of widget.""" - - if selected is None: - selected = not self._selected - - # Clear instance view selection on deselect - if not selected: - self._instances_view.clearSelection() - - # Skip if has same value - if selected == self._selected: - return - - self._selected = selected - self._change_style_property(selected) - if selected: - self.selected.emit(self._index) - self._set_expanded(True) - - def _on_toggle_btn_click(self): - """Show/hide instances list.""" - - self._set_expanded() - - def _set_expanded(self, expanded=None): - if expanded is None: - expanded = not self._expanded - - elif expanded is self._expanded: - return - - if expanded and self._context_validation: - return - - self._expanded = expanded - self._view_widget.setVisible(expanded) - if expanded: - self._toggle_instance_btn.setArrowType(QtCore.Qt.DownArrow) - else: - self._toggle_instance_btn.setArrowType(QtCore.Qt.RightArrow) - - def _on_seleciton_change(self): - sel_model = self._instances_view.selectionModel() - if sel_model.selectedIndexes(): - self.instance_changed.emit(self._index) - - -class ActionButton(BaseClickableFrame): - """Plugin's action callback button. - - Action may have label or icon or both. - - Args: - plugin_action_item (PublishPluginActionItem): Action item that can be - triggered by it's id. - """ - - action_clicked = QtCore.Signal(str, str) - - def __init__(self, plugin_action_item, parent): - super(ActionButton, self).__init__(parent) - - self.setObjectName("ValidationActionButton") - - self.plugin_action_item = plugin_action_item - - action_label = plugin_action_item.label - action_icon = plugin_action_item.icon - label_widget = QtWidgets.QLabel(action_label, self) - icon_label = None - if action_icon: - icon_label = IconValuePixmapLabel(action_icon, self) - - layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(5, 0, 5, 0) - layout.addWidget(label_widget, 1) - if icon_label: - layout.addWidget(icon_label, 0) - - self.setSizePolicy( - QtWidgets.QSizePolicy.Minimum, - self.sizePolicy().verticalPolicy() - ) - - def _mouse_release_callback(self): - self.action_clicked.emit( - self.plugin_action_item.plugin_id, - self.plugin_action_item.action_id - ) - - -class ValidateActionsWidget(QtWidgets.QFrame): - """Wrapper widget for plugin actions. - - Change actions based on selected validation error. - """ - - def __init__(self, controller, parent): - super(ValidateActionsWidget, self).__init__(parent) - - self.setAttribute(QtCore.Qt.WA_TranslucentBackground) - - content_widget = QtWidgets.QWidget(self) - content_layout = QtWidgets.QVBoxLayout(content_widget) - - layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(content_widget) - - self._controller = controller - self._content_widget = content_widget - self._content_layout = content_layout - self._actions_mapping = {} - - def clear(self): - """Remove actions from widget.""" - while self._content_layout.count(): - item = self._content_layout.takeAt(0) - widget = item.widget() - if widget: - widget.setVisible(False) - widget.deleteLater() - self._actions_mapping = {} - - def set_error_item(self, error_item): - """Set selected plugin and show it's actions. - - Clears current actions from widget and recreate them from the plugin. - - Args: - Dict[str, Any]: Object holding error items, title and possible - actions to run. - """ - - self.clear() - - if not error_item: - self.setVisible(False) - return - - plugin_action_items = error_item["plugin_action_items"] - for plugin_action_item in plugin_action_items: - if not plugin_action_item.active: - continue - - if plugin_action_item.on_filter not in ("failed", "all"): - continue - - action_id = plugin_action_item.action_id - self._actions_mapping[action_id] = plugin_action_item - - action_btn = ActionButton(plugin_action_item, self._content_widget) - action_btn.action_clicked.connect(self._on_action_click) - self._content_layout.addWidget(action_btn) - - if self._content_layout.count() > 0: - self.setVisible(True) - self._content_layout.addStretch(1) - else: - self.setVisible(False) - - def _on_action_click(self, plugin_id, action_id): - self._controller.run_action(plugin_id, action_id) - - -class VerticallScrollArea(QtWidgets.QScrollArea): - """Scroll area for validation error titles. - - The biggest difference is that the scroll area has scroll bar on left side - and resize of content will also resize scrollarea itself. - - Resize if deferred by 100ms because at the moment of resize are not yet - propagated sizes and visibility of scroll bars. - """ - - def __init__(self, *args, **kwargs): - super(VerticallScrollArea, self).__init__(*args, **kwargs) - - self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) - self.setLayoutDirection(QtCore.Qt.RightToLeft) - - self.setAttribute(QtCore.Qt.WA_TranslucentBackground) - # Background of scrollbar will be transparent - scrollbar_bg = self.verticalScrollBar().parent() - if scrollbar_bg: - scrollbar_bg.setAttribute(QtCore.Qt.WA_TranslucentBackground) - self.setViewportMargins(0, 0, 0, 0) - - self.verticalScrollBar().installEventFilter(self) - - # Timer with 100ms offset after changing size - size_changed_timer = QtCore.QTimer() - size_changed_timer.setInterval(100) - size_changed_timer.setSingleShot(True) - - size_changed_timer.timeout.connect(self._on_timer_timeout) - self._size_changed_timer = size_changed_timer - - def setVerticalScrollBar(self, widget): - old_widget = self.verticalScrollBar() - if old_widget: - old_widget.removeEventFilter(self) - - super(VerticallScrollArea, self).setVerticalScrollBar(widget) - if widget: - widget.installEventFilter(self) - - def setWidget(self, widget): - old_widget = self.widget() - if old_widget: - old_widget.removeEventFilter(self) - - super(VerticallScrollArea, self).setWidget(widget) - if widget: - widget.installEventFilter(self) - - def _on_timer_timeout(self): - width = self.widget().width() - if self.verticalScrollBar().isVisible(): - width += self.verticalScrollBar().width() - self.setMinimumWidth(width) - - def eventFilter(self, obj, event): - if ( - event.type() == QtCore.QEvent.Resize - and (obj is self.widget() or obj is self.verticalScrollBar()) - ): - self._size_changed_timer.start() - return super(VerticallScrollArea, self).eventFilter(obj, event) - - -class ValidationArtistMessage(QtWidgets.QWidget): - def __init__(self, message, parent): - super(ValidationArtistMessage, self).__init__(parent) - - artist_msg_label = QtWidgets.QLabel(message, self) - artist_msg_label.setAlignment(QtCore.Qt.AlignCenter) - - main_layout = QtWidgets.QHBoxLayout(self) - main_layout.setContentsMargins(0, 0, 0, 0) - main_layout.addWidget( - artist_msg_label, 1, QtCore.Qt.AlignCenter - ) - - -class ValidationsWidget(QtWidgets.QFrame): - """Widgets showing validation error. - - This widget is shown if validation error/s happened during validation part. - - Shows validation error titles with instances on which happened and - validation error detail with possible actions (repair). - - β”Œβ”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β” - β”‚titlesβ”‚ β”‚actionsβ”‚ - β”‚ β”‚ β”‚ β”‚ - β”‚ β”‚ Error detail β”‚ β”‚ - β”‚ β”‚ β”‚ β”‚ - β”‚ β”‚ β”‚ β”‚ - β””β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”˜ - """ - - def __init__(self, controller, parent): - super(ValidationsWidget, self).__init__(parent) - - # Before publishing - before_publish_widget = ValidationArtistMessage( - "Nothing to report until you run publish", self - ) - # After success publishing - publish_started_widget = ValidationArtistMessage( - "So far so good", self - ) - # After success publishing - publish_stop_ok_widget = ValidationArtistMessage( - "Publishing finished successfully", self - ) - # After failed publishing (not with validation error) - publish_stop_fail_widget = ValidationArtistMessage( - "This is not your fault...", self - ) - - # Validation errors - validations_widget = QtWidgets.QWidget(self) - - content_widget = QtWidgets.QWidget(validations_widget) - - errors_scroll = VerticallScrollArea(content_widget) - errors_scroll.setWidgetResizable(True) - - errors_widget = QtWidgets.QWidget(errors_scroll) - errors_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) - errors_layout = QtWidgets.QVBoxLayout(errors_widget) - errors_layout.setContentsMargins(0, 0, 0, 0) - - errors_scroll.setWidget(errors_widget) - - error_details_frame = QtWidgets.QFrame(content_widget) - error_details_input = QtWidgets.QTextEdit(error_details_frame) - error_details_input.setObjectName("InfoText") - error_details_input.setTextInteractionFlags( - QtCore.Qt.TextBrowserInteraction - ) - - actions_widget = ValidateActionsWidget(controller, content_widget) - actions_widget.setMinimumWidth(140) - - error_details_layout = QtWidgets.QHBoxLayout(error_details_frame) - error_details_layout.addWidget(error_details_input, 1) - error_details_layout.addWidget(actions_widget, 0) - - content_layout = QtWidgets.QHBoxLayout(content_widget) - content_layout.setSpacing(0) - content_layout.setContentsMargins(0, 0, 0, 0) - - content_layout.addWidget(errors_scroll, 0) - content_layout.addWidget(error_details_frame, 1) - - top_label = QtWidgets.QLabel( - "Publish validation report", content_widget - ) - top_label.setObjectName("PublishInfoMainLabel") - top_label.setAlignment(QtCore.Qt.AlignCenter) - - validation_layout = QtWidgets.QVBoxLayout(validations_widget) - validation_layout.setContentsMargins(0, 0, 0, 0) - validation_layout.addWidget(top_label, 0) - validation_layout.addWidget(content_widget, 1) - - main_layout = QtWidgets.QStackedLayout(self) - main_layout.addWidget(before_publish_widget) - main_layout.addWidget(publish_started_widget) - main_layout.addWidget(publish_stop_ok_widget) - main_layout.addWidget(publish_stop_fail_widget) - main_layout.addWidget(validations_widget) - - main_layout.setCurrentWidget(before_publish_widget) - - controller.event_system.add_callback( - "publish.process.started", self._on_publish_start - ) - controller.event_system.add_callback( - "publish.reset.finished", self._on_publish_reset - ) - controller.event_system.add_callback( - "publish.process.stopped", self._on_publish_stop - ) - - self._main_layout = main_layout - - self._before_publish_widget = before_publish_widget - self._publish_started_widget = publish_started_widget - self._publish_stop_ok_widget = publish_stop_ok_widget - self._publish_stop_fail_widget = publish_stop_fail_widget - self._validations_widget = validations_widget - - self._top_label = top_label - self._errors_widget = errors_widget - self._errors_layout = errors_layout - self._error_details_frame = error_details_frame - self._error_details_input = error_details_input - self._actions_widget = actions_widget - - self._title_widgets = {} - self._error_info = {} - self._previous_select = None - - self._controller = controller - - def clear(self): - """Delete all dynamic widgets and hide all wrappers.""" - self._title_widgets = {} - self._error_info = {} - self._previous_select = None - while self._errors_layout.count(): - item = self._errors_layout.takeAt(0) - widget = item.widget() - if widget: - widget.deleteLater() - - self._top_label.setVisible(False) - self._error_details_frame.setVisible(False) - self._errors_widget.setVisible(False) - self._actions_widget.setVisible(False) - - def _set_errors(self, validation_error_report): - """Set errors into context and created titles. - - Args: - validation_error_report (PublishValidationErrorsReport): Report - with information about validation errors and publish plugin - actions. - """ - - self.clear() - if not validation_error_report: - return - - self._top_label.setVisible(True) - self._error_details_frame.setVisible(True) - self._errors_widget.setVisible(True) - - grouped_error_items = validation_error_report.group_items_by_title() - for idx, error_info in enumerate(grouped_error_items): - widget = ValidationErrorTitleWidget(idx, error_info, self) - widget.selected.connect(self._on_select) - widget.instance_changed.connect(self._on_instance_change) - self._errors_layout.addWidget(widget) - self._title_widgets[idx] = widget - self._error_info[idx] = error_info - - self._errors_layout.addStretch(1) - - if self._title_widgets: - self._title_widgets[0].set_selected(True) - - self.updateGeometry() - - def _set_current_widget(self, widget): - self._main_layout.setCurrentWidget(widget) - - def _on_publish_start(self): - self._set_current_widget(self._publish_started_widget) - - def _on_publish_reset(self): - self._set_current_widget(self._before_publish_widget) - - def _on_publish_stop(self): - if self._controller.publish_has_crashed: - self._set_current_widget(self._publish_stop_fail_widget) - return - - if self._controller.publish_has_validation_errors: - validation_errors = self._controller.get_validation_errors() - self._set_current_widget(self._validations_widget) - self._set_errors(validation_errors) - return - - if self._controller.publish_has_finished: - self._set_current_widget(self._publish_stop_ok_widget) - return - - self._set_current_widget(self._publish_started_widget) - - def _on_select(self, index): - if self._previous_select: - if self._previous_select.index == index: - return - self._previous_select.set_selected(False) - - self._previous_select = self._title_widgets[index] - - error_item = self._error_info[index] - - self._actions_widget.set_error_item(error_item) - - self._update_description() - - def _on_instance_change(self, index): - if self._previous_select and self._previous_select.index != index: - self._title_widgets[index].set_selected(True) - else: - self._update_description() - - def _update_description(self): - description = self._previous_select.current_description_text() - if commonmark: - html = commonmark.commonmark(description) - self._error_details_input.setHtml(html) - elif hasattr(self._error_details_input, "setMarkdown"): - self._error_details_input.setMarkdown(description) - else: - self._error_details_input.setText(description) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index cd1f1f5a96..0b13f26d57 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -40,6 +40,41 @@ from ..constants import ( INPUTS_LAYOUT_VSPACING, ) +FA_PREFIXES = ["", "fa.", "fa5.", "fa5b.", "fa5s.", "ei.", "mdi."] + + +def parse_icon_def( + icon_def, default_width=None, default_height=None, color=None +): + if not icon_def: + return None + + if isinstance(icon_def, QtGui.QPixmap): + return icon_def + + color = color or "white" + default_width = default_width or 512 + default_height = default_height or 512 + + if isinstance(icon_def, QtGui.QIcon): + return icon_def.pixmap(default_width, default_height) + + try: + if os.path.exists(icon_def): + return QtGui.QPixmap(icon_def) + except Exception: + # TODO logging + pass + + for prefix in FA_PREFIXES: + try: + icon_name = "{}{}".format(prefix, icon_def) + icon = qtawesome.icon(icon_name, color=color) + return icon.pixmap(default_width, default_height) + except Exception: + # TODO logging + continue + class PublishPixmapLabel(PixmapLabel): def _get_pix_size(self): @@ -54,7 +89,6 @@ class IconValuePixmapLabel(PublishPixmapLabel): Handle icon parsing from creators/instances. Using of QAwesome module of path to images. """ - fa_prefixes = ["", "fa."] default_size = 200 def __init__(self, icon_def, parent): @@ -77,31 +111,9 @@ class IconValuePixmapLabel(PublishPixmapLabel): return pix def _parse_icon_def(self, icon_def): - if not icon_def: - return self._default_pixmap() - - if isinstance(icon_def, QtGui.QPixmap): - return icon_def - - if isinstance(icon_def, QtGui.QIcon): - return icon_def.pixmap(self.default_size, self.default_size) - - try: - if os.path.exists(icon_def): - return QtGui.QPixmap(icon_def) - except Exception: - # TODO logging - pass - - for prefix in self.fa_prefixes: - try: - icon_name = "{}{}".format(prefix, icon_def) - icon = qtawesome.icon(icon_name, color="white") - return icon.pixmap(self.default_size, self.default_size) - except Exception: - # TODO logging - continue - + icon = parse_icon_def(icon_def, self.default_size, self.default_size) + if icon: + return icon return self._default_pixmap() @@ -692,6 +704,7 @@ class TasksCombobox(QtWidgets.QComboBox): style.drawControl( QtWidgets.QStyle.CE_ComboBoxLabel, opt, painter, self ) + painter.end() def is_valid(self): """Are all selected items valid.""" diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index b3471163ae..fc90e66f21 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -1,3 +1,6 @@ +import os +import json +import time import collections import copy from qtpy import QtWidgets, QtCore, QtGui @@ -15,10 +18,11 @@ from openpype.tools.utils import ( from .constants import ResetKeySequence from .publish_report_viewer import PublishReportViewerWidget +from .control import CardMessageTypes from .control_qt import QtPublisherController from .widgets import ( OverviewWidget, - ValidationsWidget, + ReportPageWidget, PublishFrame, PublisherTabsWidget, @@ -182,7 +186,7 @@ class PublisherWindow(QtWidgets.QDialog): controller, content_stacked_widget ) - report_widget = ValidationsWidget(controller, parent) + report_widget = ReportPageWidget(controller, parent) # Details - Publish details publish_details_widget = PublishReportViewerWidget( @@ -313,6 +317,13 @@ class PublisherWindow(QtWidgets.QDialog): controller.event_system.add_callback( "convertors.find.failed", self._on_convertor_error ) + controller.event_system.add_callback( + "export_report.request", self._export_report + ) + controller.event_system.add_callback( + "copy_report.request", self._copy_report + ) + # Store extra header widget for TrayPublisher # - can be used to add additional widgets to header between context @@ -825,6 +836,9 @@ class PublisherWindow(QtWidgets.QDialog): self._validate_btn.setEnabled(validate_enabled) self._publish_btn.setEnabled(publish_enabled) + if not publish_enabled: + self._publish_frame.set_shrunk_state(True) + self._update_publish_details_widget() def _validate_create_instances(self): @@ -941,6 +955,46 @@ class PublisherWindow(QtWidgets.QDialog): under_mouse = widget_x < global_pos.x() self._create_overlay_button.set_under_mouse(under_mouse) + def _copy_report(self): + logs = self._controller.get_publish_report() + logs_string = json.dumps(logs, indent=4) + + mime_data = QtCore.QMimeData() + mime_data.setText(logs_string) + QtWidgets.QApplication.instance().clipboard().setMimeData( + mime_data + ) + self._controller.emit_card_message( + "Report added to clipboard", + CardMessageTypes.info) + + def _export_report(self): + default_filename = "publish-report-{}".format( + time.strftime("%y%m%d-%H-%M") + ) + default_filepath = os.path.join( + os.path.expanduser("~"), + default_filename + ) + new_filepath, ext = QtWidgets.QFileDialog.getSaveFileName( + self, "Save report", default_filepath, ".json" + ) + if not ext or not new_filepath: + return + + logs = self._controller.get_publish_report() + full_path = new_filepath + ext + dir_path = os.path.dirname(full_path) + if not os.path.exists(dir_path): + os.makedirs(dir_path) + + with open(full_path, "w") as file_stream: + json.dump(logs, file_stream) + + self._controller.emit_card_message( + "Report saved", + CardMessageTypes.info) + class ErrorsMessageBox(ErrorMessageBox): def __init__(self, error_title, failed_info, message_start, parent): diff --git a/openpype/tools/utils/__init__.py b/openpype/tools/utils/__init__.py index 4149763f80..10bd527692 100644 --- a/openpype/tools/utils/__init__.py +++ b/openpype/tools/utils/__init__.py @@ -1,13 +1,16 @@ +from .layouts import FlowLayout from .widgets import ( FocusSpinBox, FocusDoubleSpinBox, ComboBox, CustomTextComboBox, PlaceholderLineEdit, + ExpandingTextEdit, BaseClickableFrame, ClickableFrame, ClickableLabel, ExpandBtn, + ClassicExpandBtn, PixmapLabel, IconButton, PixmapButton, @@ -37,15 +40,19 @@ from .overlay_messages import ( __all__ = ( + "FlowLayout", + "FocusSpinBox", "FocusDoubleSpinBox", "ComboBox", "CustomTextComboBox", "PlaceholderLineEdit", + "ExpandingTextEdit", "BaseClickableFrame", "ClickableFrame", "ClickableLabel", "ExpandBtn", + "ClassicExpandBtn", "PixmapLabel", "IconButton", "PixmapButton", diff --git a/openpype/tools/utils/layouts.py b/openpype/tools/utils/layouts.py new file mode 100644 index 0000000000..65ea087c27 --- /dev/null +++ b/openpype/tools/utils/layouts.py @@ -0,0 +1,150 @@ +from qtpy import QtWidgets, QtCore + + +class FlowLayout(QtWidgets.QLayout): + """Layout that organize widgets by minimum size into a flow layout. + + Layout is putting widget from left to right and top to bottom. When widget + can't fit a row it is added to next line. Minimum size matches widget with + biggest 'sizeHint' width and height using calculated geometry. + + Content margins are part of calculations. It is possible to define + horizontal and vertical spacing. + + Layout does not support stretch and spacing items. + + Todos: + Unified width concept -> use width of largest item so all of them are + same. This could allow to have minimum columns option too. + """ + + def __init__(self, parent=None): + super(FlowLayout, self).__init__(parent) + + # spaces between each item + self._horizontal_spacing = 5 + self._vertical_spacing = 5 + + self._items = [] + + def __del__(self): + while self.count(): + self.takeAt(0, False) + + def isEmpty(self): + for item in self._items: + if not item.isEmpty(): + return False + return True + + def setSpacing(self, spacing): + self._horizontal_spacing = spacing + self._vertical_spacing = spacing + self.invalidate() + + def setHorizontalSpacing(self, spacing): + self._horizontal_spacing = spacing + self.invalidate() + + def setVerticalSpacing(self, spacing): + self._vertical_spacing = spacing + self.invalidate() + + def addItem(self, item): + self._items.append(item) + self.invalidate() + + def count(self): + return len(self._items) + + def itemAt(self, index): + if 0 <= index < len(self._items): + return self._items[index] + return None + + def takeAt(self, index, invalidate=True): + if 0 <= index < len(self._items): + item = self._items.pop(index) + if invalidate: + self.invalidate() + return item + return None + + def expandingDirections(self): + return QtCore.Qt.Orientations(QtCore.Qt.Vertical) + + def hasHeightForWidth(self): + return True + + def heightForWidth(self, width): + return self._setup_geometry(QtCore.QRect(0, 0, width, 0), True) + + def setGeometry(self, rect): + super(FlowLayout, self).setGeometry(rect) + self._setup_geometry(rect) + + def sizeHint(self): + return self.minimumSize() + + def minimumSize(self): + size = QtCore.QSize(0, 0) + for item in self._items: + widget = item.widget() + if widget is not None: + parent = widget.parent() + if not widget.isVisibleTo(parent): + continue + size = size.expandedTo(item.minimumSize()) + + if size.width() < 1 or size.height() < 1: + return size + l_margin, t_margin, r_margin, b_margin = self.getContentsMargins() + size += QtCore.QSize(l_margin + r_margin, t_margin + b_margin) + return size + + def _setup_geometry(self, rect, only_calculate=False): + h_spacing = self._horizontal_spacing + v_spacing = self._vertical_spacing + l_margin, t_margin, r_margin, b_margin = self.getContentsMargins() + + left_x = rect.x() + l_margin + top_y = rect.y() + t_margin + pos_x = left_x + pos_y = top_y + row_height = 0 + for item in self._items: + item_hint = item.sizeHint() + item_width = item_hint.width() + item_height = item_hint.height() + if item_width < 1 or item_height < 1: + continue + + end_x = pos_x + item_width + + wrap = ( + row_height > 0 + and ( + end_x > rect.right() + or (end_x + r_margin) > rect.right() + ) + ) + if not wrap: + next_pos_x = end_x + h_spacing + else: + pos_x = left_x + pos_y += row_height + v_spacing + next_pos_x = pos_x + item_width + h_spacing + row_height = 0 + + if not only_calculate: + item.setGeometry( + QtCore.QRect(pos_x, pos_y, item_width, item_height) + ) + + pos_x = next_pos_x + row_height = max(row_height, item_height) + + height = (pos_y - top_y) + row_height + if height > 0: + height += b_margin + return height diff --git a/openpype/tools/utils/widgets.py b/openpype/tools/utils/widgets.py index bae89aeb09..5a8104611b 100644 --- a/openpype/tools/utils/widgets.py +++ b/openpype/tools/utils/widgets.py @@ -101,6 +101,46 @@ class PlaceholderLineEdit(QtWidgets.QLineEdit): self.setPalette(filter_palette) +class ExpandingTextEdit(QtWidgets.QTextEdit): + """QTextEdit which does not have sroll area but expands height.""" + + def __init__(self, parent=None): + super(ExpandingTextEdit, self).__init__(parent) + + size_policy = self.sizePolicy() + size_policy.setHeightForWidth(True) + size_policy.setVerticalPolicy(QtWidgets.QSizePolicy.Preferred) + self.setSizePolicy(size_policy) + + self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + + doc = self.document() + doc.contentsChanged.connect(self._on_doc_change) + + def _on_doc_change(self): + self.updateGeometry() + + def hasHeightForWidth(self): + return True + + def heightForWidth(self, width): + margins = self.contentsMargins() + + document_width = 0 + if width >= margins.left() + margins.right(): + document_width = width - margins.left() - margins.right() + + document = self.document().clone() + document.setTextWidth(document_width) + + return margins.top() + document.size().height() + margins.bottom() + + def sizeHint(self): + width = super(ExpandingTextEdit, self).sizeHint().width() + return QtCore.QSize(width, self.heightForWidth(width)) + + class BaseClickableFrame(QtWidgets.QFrame): """Widget that catch left mouse click and can trigger a callback. @@ -161,19 +201,34 @@ class ClickableLabel(QtWidgets.QLabel): class ExpandBtnLabel(QtWidgets.QLabel): """Label showing expand icon meant for ExpandBtn.""" + state_changed = QtCore.Signal() + + def __init__(self, parent): super(ExpandBtnLabel, self).__init__(parent) - self._source_collapsed_pix = QtGui.QPixmap( - get_style_image_path("branch_closed") - ) - self._source_expanded_pix = QtGui.QPixmap( - get_style_image_path("branch_open") - ) + self._source_collapsed_pix = self._create_collapsed_pixmap() + self._source_expanded_pix = self._create_expanded_pixmap() self._current_image = self._source_collapsed_pix self._collapsed = True - def set_collapsed(self, collapsed): + def _create_collapsed_pixmap(self): + return QtGui.QPixmap( + get_style_image_path("branch_closed") + ) + + def _create_expanded_pixmap(self): + return QtGui.QPixmap( + get_style_image_path("branch_open") + ) + + @property + def collapsed(self): + return self._collapsed + + def set_collapsed(self, collapsed=None): + if collapsed is None: + collapsed = not self._collapsed if self._collapsed == collapsed: return self._collapsed = collapsed @@ -182,6 +237,7 @@ class ExpandBtnLabel(QtWidgets.QLabel): else: self._current_image = self._source_expanded_pix self._set_resized_pix() + self.state_changed.emit() def resizeEvent(self, event): self._set_resized_pix() @@ -203,21 +259,55 @@ class ExpandBtnLabel(QtWidgets.QLabel): class ExpandBtn(ClickableFrame): + state_changed = QtCore.Signal() + def __init__(self, parent=None): super(ExpandBtn, self).__init__(parent) - pixmap_label = ExpandBtnLabel(self) + pixmap_label = self._create_pix_widget(self) layout = QtWidgets.QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(pixmap_label) + pixmap_label.state_changed.connect(self.state_changed) + self._pixmap_label = pixmap_label - def set_collapsed(self, collapsed): + def _create_pix_widget(self, parent=None): + if parent is None: + parent = self + return ExpandBtnLabel(parent) + + @property + def collapsed(self): + return self._pixmap_label.collapsed + + def set_collapsed(self, collapsed=None): self._pixmap_label.set_collapsed(collapsed) +class ClassicExpandBtnLabel(ExpandBtnLabel): + def _create_collapsed_pixmap(self): + return QtGui.QPixmap( + get_style_image_path("right_arrow") + ) + + def _create_expanded_pixmap(self): + return QtGui.QPixmap( + get_style_image_path("down_arrow") + ) + + +class ClassicExpandBtn(ExpandBtn): + """Same as 'ExpandBtn' but with arrow images.""" + + def _create_pix_widget(self, parent=None): + if parent is None: + parent = self + return ClassicExpandBtnLabel(parent) + + class ImageButton(QtWidgets.QPushButton): """PushButton with icon and size of font. From 96a4edf8cb412906047be7435a742ec80e2f4b94 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 23 May 2023 23:02:52 +0200 Subject: [PATCH 117/189] Resolve: fixing the issue with no active timeline during bootstrap of loader --- openpype/hosts/resolve/api/lib.py | 32 ++++++++++++++++--- .../hosts/resolve/plugins/load/load_clip.py | 1 + 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index b3ad20df39..1c33749a77 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -91,16 +91,39 @@ def get_current_project(): return self.project_manager.GetCurrentProject() -def get_current_timeline(new=False): +def get_current_timeline(any=False, new=False): + """Get current timeline object. + + Args: + any (bool, optional): return any even new if no timeline available. + Defaults to False. + new (bool, optional): return only new timeline. Defaults to False. + + Returns: + _type_: _description_ + """ # get current project project = get_current_project() + timeline = project.GetCurrentTimeline() + + # return current timeline only if it is not new + if timeline and not new: + return timeline + + # if any is True then return any timeline + if any: + timeline_count = project.GetTimelineCount() + if timeline_count == 0: + # if there is no timeline then create a new one + new = True + + # create new timeline if new is True if new: media_pool = project.GetMediaPool() new_timeline = media_pool.CreateEmptyTimeline(self.pype_timeline_name) project.SetCurrentTimeline(new_timeline) - - return project.GetCurrentTimeline() + return new_timeline def create_bin(name: str, root: object = None) -> object: @@ -312,7 +335,8 @@ def get_current_timeline_items( track_type = track_type or "video" selecting_color = selecting_color or "Chocolate" project = get_current_project() - timeline = get_current_timeline() + # make sure some timeline will be active with `any` argument + timeline = get_current_timeline(any=True) selected_clips = [] # get all tracks count filtered by track type diff --git a/openpype/hosts/resolve/plugins/load/load_clip.py b/openpype/hosts/resolve/plugins/load/load_clip.py index d30a7ea272..05bfb003d6 100644 --- a/openpype/hosts/resolve/plugins/load/load_clip.py +++ b/openpype/hosts/resolve/plugins/load/load_clip.py @@ -19,6 +19,7 @@ from openpype.lib.transcoding import ( IMAGE_EXTENSIONS ) + class LoadClip(plugin.TimelineItemLoader): """Load a subset to timeline as clip From 179dc65f501faa6e71d5e783ef0d01f0d1ac09aa Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 24 May 2023 03:25:50 +0000 Subject: [PATCH 118/189] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index 8874eb510d..3d7f64b991 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.8-nightly.2" +__version__ = "3.15.8-nightly.3" From ea2d87d903ed95200e1ceb121440e690b65907e0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 24 May 2023 03:26:39 +0000 Subject: [PATCH 119/189] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 244eb1a363..a9f1f1cc02 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.15.8-nightly.3 - 3.15.8-nightly.2 - 3.15.8-nightly.1 - 3.15.7 @@ -134,7 +135,6 @@ body: - 3.14.2-nightly.3 - 3.14.2-nightly.2 - 3.14.2-nightly.1 - - 3.14.1 validations: required: true - type: dropdown From fe02a093128b17a02d0c2e87405c8beca7bc23e4 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 24 May 2023 10:01:59 +0200 Subject: [PATCH 120/189] Deadline: fix selection from multiple webservices (#5015) * OP-4380 - override default DL from project settings * OP-4380 - updated documentation --- .../collect_default_deadline_server.py | 26 ++++++++++++++++++- website/docs/module_deadline.md | 3 +++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/collect_default_deadline_server.py b/openpype/modules/deadline/plugins/publish/collect_default_deadline_server.py index e6ad6a9aa1..cb2b0cf156 100644 --- a/openpype/modules/deadline/plugins/publish/collect_default_deadline_server.py +++ b/openpype/modules/deadline/plugins/publish/collect_default_deadline_server.py @@ -4,7 +4,18 @@ import pyblish.api class CollectDefaultDeadlineServer(pyblish.api.ContextPlugin): - """Collect default Deadline Webservice URL.""" + """Collect default Deadline Webservice URL. + + DL webservice addresses must be configured first in System Settings for + project settings enum to work. + + Default webservice could be overriden by + `project_settings/deadline/deadline_servers`. Currently only single url + is expected. + + This url could be overriden by some hosts directly on instances with + `CollectDeadlineServerFromInstance`. + """ order = pyblish.api.CollectorOrder + 0.410 label = "Default Deadline Webservice" @@ -23,3 +34,16 @@ class CollectDefaultDeadlineServer(pyblish.api.ContextPlugin): context.data["defaultDeadline"] = deadline_module.deadline_urls["default"] # noqa: E501 context.data["deadlinePassMongoUrl"] = self.pass_mongo_url + + deadline_servers = (context.data + ["project_settings"] + ["deadline"] + ["deadline_servers"]) + if deadline_servers: + deadline_server_name = deadline_servers[0] + deadline_webservice = deadline_module.deadline_urls.get( + deadline_server_name) + if deadline_webservice: + context.data["defaultDeadline"] = deadline_webservice + self.log.debug("Overriding from project settings with {}".format( # noqa: E501 + deadline_webservice)) diff --git a/website/docs/module_deadline.md b/website/docs/module_deadline.md index 94b6a381c2..bca2a83936 100644 --- a/website/docs/module_deadline.md +++ b/website/docs/module_deadline.md @@ -22,6 +22,9 @@ For [AWS Thinkbox Deadline](https://www.awsthinkbox.com/deadline) support you ne 5. Install our custom plugin and scripts to your deadline repository. It should be as simple as copying content of `openpype/modules/deadline/repository/custom` to `path/to/your/deadline/repository/custom`. +Multiple different DL webservice could be configured. First set them in point 4., then they could be configured per project in `project_settings/deadline/deadline_servers`. +Only single webservice could be a target of publish though. + ## Configuration From 248336bb0ddf2a61632e579600021b093c24440f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 24 May 2023 10:51:35 +0200 Subject: [PATCH 121/189] General: Lib code cleanup (#5003) * implemented 'is_func_signature_supported' function * 'WeakMethod' can be imported from 'python_2_comp' all the time * simplified events logic for callback registration * modified docstrings in publish lib * removed unused imports * fixed 'run_openpype_process' docstring --- openpype/lib/__init__.py | 6 ++- openpype/lib/events.py | 43 +++--------------- openpype/lib/execute.py | 2 +- openpype/lib/python_2_comp.py | 65 +++++++++++++++------------- openpype/lib/python_module_tools.py | 67 +++++++++++++++++++++++++++++ openpype/pipeline/publish/lib.py | 34 ++++++++------- 6 files changed, 129 insertions(+), 88 deletions(-) diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 9eb7724a60..06de486f2e 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # flake8: noqa E402 -"""Pype module API.""" +"""OpenPype lib functions.""" # add vendor to sys path based on Python version import sys import os @@ -94,7 +94,8 @@ from .python_module_tools import ( modules_from_path, recursive_bases_from_class, classes_from_module, - import_module_from_dirpath + import_module_from_dirpath, + is_func_signature_supported, ) from .profiles_filtering import ( @@ -243,6 +244,7 @@ __all__ = [ "recursive_bases_from_class", "classes_from_module", "import_module_from_dirpath", + "is_func_signature_supported", "get_transcode_temp_directory", "should_convert_for_ffmpeg", diff --git a/openpype/lib/events.py b/openpype/lib/events.py index bed00fe659..dca58fcf93 100644 --- a/openpype/lib/events.py +++ b/openpype/lib/events.py @@ -6,10 +6,9 @@ import inspect import logging import weakref from uuid import uuid4 -try: - from weakref import WeakMethod -except Exception: - from openpype.lib.python_2_comp import WeakMethod + +from .python_2_comp import WeakMethod +from .python_module_tools import is_func_signature_supported class MissingEventSystem(Exception): @@ -80,40 +79,8 @@ class EventCallback(object): # Get expected arguments from function spec # - positional arguments are always preferred - expect_args = False - expect_kwargs = False - fake_event = "fake" - if hasattr(inspect, "signature"): - # Python 3 using 'Signature' object where we try to bind arg - # or kwarg. Using signature is recommended approach based on - # documentation. - sig = inspect.signature(func) - try: - sig.bind(fake_event) - expect_args = True - except TypeError: - pass - - try: - sig.bind(event=fake_event) - expect_kwargs = True - except TypeError: - pass - - else: - # In Python 2 'signature' is not available so 'getcallargs' is used - # - 'getcallargs' is marked as deprecated since Python 3.0 - try: - inspect.getcallargs(func, fake_event) - expect_args = True - except TypeError: - pass - - try: - inspect.getcallargs(func, event=fake_event) - expect_kwargs = True - except TypeError: - pass + expect_args = is_func_signature_supported(func, "fake") + expect_kwargs = is_func_signature_supported(func, event="fake") self._func_ref = func_ref self._func_name = func_name diff --git a/openpype/lib/execute.py b/openpype/lib/execute.py index ef456395e7..6f52efdfcc 100644 --- a/openpype/lib/execute.py +++ b/openpype/lib/execute.py @@ -190,7 +190,7 @@ def run_openpype_process(*args, **kwargs): Example: ``` - run_openpype_process("run", "") + run_detached_process("run", "") ``` Args: diff --git a/openpype/lib/python_2_comp.py b/openpype/lib/python_2_comp.py index d7137dbe9c..091c51a6f6 100644 --- a/openpype/lib/python_2_comp.py +++ b/openpype/lib/python_2_comp.py @@ -1,41 +1,44 @@ import weakref -class _weak_callable: - def __init__(self, obj, func): - self.im_self = obj - self.im_func = func +WeakMethod = getattr(weakref, "WeakMethod", None) - def __call__(self, *args, **kws): - if self.im_self is None: - return self.im_func(*args, **kws) - else: - return self.im_func(self.im_self, *args, **kws) +if WeakMethod is None: + class _WeakCallable: + def __init__(self, obj, func): + self.im_self = obj + self.im_func = func + + def __call__(self, *args, **kws): + if self.im_self is None: + return self.im_func(*args, **kws) + else: + return self.im_func(self.im_self, *args, **kws) -class WeakMethod: - """ Wraps a function or, more importantly, a bound method in - a way that allows a bound method's object to be GCed, while - providing the same interface as a normal weak reference. """ + class WeakMethod: + """ Wraps a function or, more importantly, a bound method in + a way that allows a bound method's object to be GCed, while + providing the same interface as a normal weak reference. """ - def __init__(self, fn): - try: - self._obj = weakref.ref(fn.im_self) - self._meth = fn.im_func - except AttributeError: - # It's not a bound method - self._obj = None - self._meth = fn + def __init__(self, fn): + try: + self._obj = weakref.ref(fn.im_self) + self._meth = fn.im_func + except AttributeError: + # It's not a bound method + self._obj = None + self._meth = fn - def __call__(self): - if self._dead(): - return None - return _weak_callable(self._getobj(), self._meth) + def __call__(self): + if self._dead(): + return None + return _WeakCallable(self._getobj(), self._meth) - def _dead(self): - return self._obj is not None and self._obj() is None + def _dead(self): + return self._obj is not None and self._obj() is None - def _getobj(self): - if self._obj is None: - return None - return self._obj() + def _getobj(self): + if self._obj is None: + return None + return self._obj() diff --git a/openpype/lib/python_module_tools.py b/openpype/lib/python_module_tools.py index 9e8e94842c..a10263f991 100644 --- a/openpype/lib/python_module_tools.py +++ b/openpype/lib/python_module_tools.py @@ -230,3 +230,70 @@ def import_module_from_dirpath(dirpath, folder_name, dst_module_name=None): dirpath, folder_name, dst_module_name ) return module + + +def is_func_signature_supported(func, *args, **kwargs): + """Check if a function signature supports passed args and kwargs. + + This check does not actually call the function, just look if function can + be called with the arguments. + + Notes: + This does NOT check if the function would work with passed arguments + only if they can be passed in. If function have *args, **kwargs + in paramaters, this will always return 'True'. + + Example: + >>> def my_function(my_number): + ... return my_number + 1 + ... + >>> is_func_signature_supported(my_function, 1) + True + >>> is_func_signature_supported(my_function, 1, 2) + False + >>> is_func_signature_supported(my_function, my_number=1) + True + >>> is_func_signature_supported(my_function, number=1) + False + >>> is_func_signature_supported(my_function, "string") + True + >>> def my_other_function(*args, **kwargs): + ... my_function(*args, **kwargs) + ... + >>> is_func_signature_supported( + ... my_other_function, + ... "string", + ... 1, + ... other=None + ... ) + True + + Args: + func (function): A function where the signature should be tested. + *args (tuple[Any]): Positional arguments for function signature. + **kwargs (dict[str, Any]): Keyword arguments for function signature. + + Returns: + bool: Function can pass in arguments. + """ + + if hasattr(inspect, "signature"): + # Python 3 using 'Signature' object where we try to bind arg + # or kwarg. Using signature is recommended approach based on + # documentation. + sig = inspect.signature(func) + try: + sig.bind(*args, **kwargs) + return True + except TypeError: + pass + + else: + # In Python 2 'signature' is not available so 'getcallargs' is used + # - 'getcallargs' is marked as deprecated since Python 3.0 + try: + inspect.getcallargs(func, *args, **kwargs) + return True + except TypeError: + pass + return False diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index 40186238aa..63a856e326 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -1,12 +1,10 @@ import os import sys -import types import inspect import copy import tempfile import xml.etree.ElementTree -import six import pyblish.util import pyblish.plugin import pyblish.api @@ -42,7 +40,9 @@ def get_template_name_profiles( Args: project_name (str): Name of project where to look for templates. - project_settings(Dic[str, Any]): Prepared project settings. + project_settings (Dict[str, Any]): Prepared project settings. + logger (Optional[logging.Logger]): Logger object to be used instead + of default logger. Returns: List[Dict[str, Any]]: Publish template profiles. @@ -103,7 +103,9 @@ def get_hero_template_name_profiles( Args: project_name (str): Name of project where to look for templates. - project_settings(Dic[str, Any]): Prepared project settings. + project_settings (Dict[str, Any]): Prepared project settings. + logger (Optional[logging.Logger]): Logger object to be used instead + of default logger. Returns: List[Dict[str, Any]]: Publish template profiles. @@ -172,9 +174,10 @@ def get_publish_template_name( project_name (str): Name of project where to look for settings. host_name (str): Name of host integration. family (str): Family for which should be found template. - task_name (str): Task name on which is intance working. - task_type (str): Task type on which is intance working. - project_setting (Dict[str, Any]): Prepared project settings. + task_name (str): Task name on which is instance working. + task_type (str): Task type on which is instance working. + project_settings (Dict[str, Any]): Prepared project settings. + hero (bool): Template is for hero version publishing. logger (logging.Logger): Custom logger used for 'filter_profiles' function. @@ -264,19 +267,18 @@ def load_help_content_from_plugin(plugin): def publish_plugins_discover(paths=None): """Find and return available pyblish plug-ins - Overridden function from `pyblish` module to be able collect crashed files - and reason of their crash. + Overridden function from `pyblish` module to be able to collect + crashed files and reason of their crash. Arguments: paths (list, optional): Paths to discover plug-ins from. If no paths are provided, all paths are searched. - """ # The only difference with `pyblish.api.discover` result = DiscoverResult(pyblish.api.Plugin) - plugins = dict() + plugins = {} plugin_names = [] allow_duplicates = pyblish.plugin.ALLOW_DUPLICATES @@ -302,7 +304,7 @@ def publish_plugins_discover(paths=None): mod_name, mod_ext = os.path.splitext(fname) - if not mod_ext == ".py": + if mod_ext != ".py": continue try: @@ -533,10 +535,10 @@ def find_close_plugin(close_plugin_name, log): def remote_publish(log, close_plugin_name=None, raise_error=False): """Loops through all plugins, logs to console. Used for tests. - Args: - log (openpype.lib.Logger) - close_plugin_name (str): name of plugin with responsibility to - close host app + Args: + log (Logger) + close_plugin_name (str): name of plugin with responsibility to + close host app """ # Error exit as soon as any error occurs. error_format = "Failed {plugin.__name__}: {error} -- {error.traceback}" From 17a38c32a4ba6e33798a097418c1f91c732d1fb8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 24 May 2023 10:54:31 +0200 Subject: [PATCH 122/189] Enhancement: Improve logging levels and messages for artist facing publish reports (#5018) * Tweak log levels and message to be more informative to artist in report page * Tweak levels and clarity of logs * Tweak levels and clarity of logs + tweak grammar * Cosmetics * Improve logging * Simplify logging * Convert to debug log if it's skipping thumbnail integration if there's no thumbnail whatsoever to integrate * Tweak to debug since they only show representation ids hardly understandable to the artist * Match logging message across hosts + include filepath for full clarity * Tweak message to clarify it only starts checking and not that it requires filling + to debug log * Tweak to debug log if there's basically no thumbnail to integrate at the end * Tweak log levels - Artist doesn't care what's prepared, especially since afterwards it's logged what gets written to the database anyway * Log clearly it's processing a legacy instance * Cosmetics --- .../fusion/plugins/publish/collect_inputs.py | 2 +- .../fusion/plugins/publish/save_scene.py | 2 +- .../houdini/plugins/publish/collect_frames.py | 7 ++-- .../houdini/plugins/publish/collect_inputs.py | 2 +- .../plugins/publish/collect_instances.py | 4 +- .../plugins/publish/collect_workfile.py | 3 +- .../houdini/plugins/publish/save_scene.py | 2 +- .../publish/validate_workfile_paths.py | 41 ++++++++++++++----- .../maya/plugins/publish/collect_inputs.py | 2 +- .../hosts/maya/plugins/publish/save_scene.py | 2 +- .../plugins/publish/save_workfile.py | 5 ++- .../plugins/publish/submit_fusion_deadline.py | 2 +- .../plugins/publish/submit_nuke_deadline.py | 2 +- .../plugins/publish/submit_publish_job.py | 2 +- .../publish/validate_deadline_pools.py | 2 +- openpype/pipeline/publish/publish_plugins.py | 13 +++--- openpype/plugins/publish/cleanup.py | 9 ++-- .../publish/collect_anatomy_context_data.py | 5 ++- .../publish/collect_anatomy_instance_data.py | 8 ++-- .../plugins/publish/collect_anatomy_object.py | 2 +- .../publish/collect_custom_staging_dir.py | 2 +- .../publish/collect_from_create_context.py | 4 +- .../plugins/publish/collect_scene_version.py | 5 ++- openpype/plugins/publish/extract_burnin.py | 4 +- .../publish/extract_color_transcode.py | 6 +-- openpype/plugins/publish/extract_review.py | 6 +-- openpype/plugins/publish/extract_thumbnail.py | 14 +++---- .../publish/extract_thumbnail_from_source.py | 4 +- openpype/plugins/publish/integrate.py | 8 ++-- openpype/plugins/publish/integrate_legacy.py | 6 ++- .../plugins/publish/integrate_thumbnail.py | 4 +- 31 files changed, 106 insertions(+), 74 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_inputs.py b/openpype/hosts/fusion/plugins/publish/collect_inputs.py index 1bb3cd1220..a6628300db 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_inputs.py +++ b/openpype/hosts/fusion/plugins/publish/collect_inputs.py @@ -113,4 +113,4 @@ class CollectUpstreamInputs(pyblish.api.InstancePlugin): inputs = [c["representation"] for c in containers] instance.data["inputRepresentations"] = inputs - self.log.info("Collected inputs: %s" % inputs) + self.log.debug("Collected inputs: %s" % inputs) diff --git a/openpype/hosts/fusion/plugins/publish/save_scene.py b/openpype/hosts/fusion/plugins/publish/save_scene.py index a249c453d8..0798e7c8b7 100644 --- a/openpype/hosts/fusion/plugins/publish/save_scene.py +++ b/openpype/hosts/fusion/plugins/publish/save_scene.py @@ -17,5 +17,5 @@ class FusionSaveComp(pyblish.api.ContextPlugin): current = comp.GetAttrs().get("COMPS_FileName", "") assert context.data['currentFile'] == current - self.log.info("Saving current file..") + self.log.info("Saving current file: {}".format(current)) comp.Save() diff --git a/openpype/hosts/houdini/plugins/publish/collect_frames.py b/openpype/hosts/houdini/plugins/publish/collect_frames.py index 6c695f64e9..059793e3c5 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_frames.py +++ b/openpype/hosts/houdini/plugins/publish/collect_frames.py @@ -8,7 +8,6 @@ import pyblish.api from openpype.hosts.houdini.api import lib - class CollectFrames(pyblish.api.InstancePlugin): """Collect all frames which would be saved from the ROP nodes""" @@ -34,8 +33,10 @@ class CollectFrames(pyblish.api.InstancePlugin): self.log.warning("Using current frame: {}".format(hou.frame())) output = output_parm.eval() - _, ext = lib.splitext(output, - allowed_multidot_extensions=[".ass.gz"]) + _, ext = lib.splitext( + output, + allowed_multidot_extensions=[".ass.gz"] + ) file_name = os.path.basename(output) result = file_name diff --git a/openpype/hosts/houdini/plugins/publish/collect_inputs.py b/openpype/hosts/houdini/plugins/publish/collect_inputs.py index 6411376ea3..e92a42f2e8 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_inputs.py +++ b/openpype/hosts/houdini/plugins/publish/collect_inputs.py @@ -117,4 +117,4 @@ class CollectUpstreamInputs(pyblish.api.InstancePlugin): inputs = [c["representation"] for c in containers] instance.data["inputRepresentations"] = inputs - self.log.info("Collected inputs: %s" % inputs) + self.log.debug("Collected inputs: %s" % inputs) diff --git a/openpype/hosts/houdini/plugins/publish/collect_instances.py b/openpype/hosts/houdini/plugins/publish/collect_instances.py index bb85630552..5d5347f96e 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_instances.py +++ b/openpype/hosts/houdini/plugins/publish/collect_instances.py @@ -55,7 +55,9 @@ class CollectInstances(pyblish.api.ContextPlugin): has_family = node.evalParm("family") assert has_family, "'%s' is missing 'family'" % node.name() - self.log.info("processing {}".format(node)) + self.log.info( + "Processing legacy instance node {}".format(node.path()) + ) data = lib.read(node) # Check bypass state and reverse diff --git a/openpype/hosts/houdini/plugins/publish/collect_workfile.py b/openpype/hosts/houdini/plugins/publish/collect_workfile.py index a6e94ec29e..aa533bcf1b 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_workfile.py +++ b/openpype/hosts/houdini/plugins/publish/collect_workfile.py @@ -32,5 +32,4 @@ class CollectWorkfile(pyblish.api.InstancePlugin): "stagingDir": folder, }] - self.log.info('Collected instance: {}'.format(file)) - self.log.info('staging Dir: {}'.format(folder)) + self.log.debug('Collected workfile instance: {}'.format(file)) diff --git a/openpype/hosts/houdini/plugins/publish/save_scene.py b/openpype/hosts/houdini/plugins/publish/save_scene.py index d6e07ccab0..703d3e4895 100644 --- a/openpype/hosts/houdini/plugins/publish/save_scene.py +++ b/openpype/hosts/houdini/plugins/publish/save_scene.py @@ -20,7 +20,7 @@ class SaveCurrentScene(pyblish.api.ContextPlugin): ) if host.has_unsaved_changes(): - self.log.info("Saving current file {}...".format(current_file)) + self.log.info("Saving current file: {}".format(current_file)) host.save_workfile(current_file) else: self.log.debug("No unsaved changes, skipping file save..") diff --git a/openpype/hosts/houdini/plugins/publish/validate_workfile_paths.py b/openpype/hosts/houdini/plugins/publish/validate_workfile_paths.py index 7707cc2dba..543c8e1407 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_workfile_paths.py +++ b/openpype/hosts/houdini/plugins/publish/validate_workfile_paths.py @@ -28,18 +28,37 @@ class ValidateWorkfilePaths( if not self.is_active(instance.data): return invalid = self.get_invalid() - self.log.info( - "node types to check: {}".format(", ".join(self.node_types))) - self.log.info( - "prohibited vars: {}".format(", ".join(self.prohibited_vars)) + self.log.debug( + "Checking node types: {}".format(", ".join(self.node_types))) + self.log.debug( + "Searching prohibited vars: {}".format( + ", ".join(self.prohibited_vars) + ) ) - if invalid: - for param in invalid: - self.log.error( - "{}: {}".format(param.path(), param.unexpandedString())) - raise PublishValidationError( - "Invalid paths found", title=self.label) + if invalid: + all_container_vars = set() + for param in invalid: + value = param.unexpandedString() + contained_vars = [ + var for var in self.prohibited_vars + if var in value + ] + all_container_vars.update(contained_vars) + + self.log.error( + "Parm {} contains prohibited vars {}: {}".format( + param.path(), + ", ".join(contained_vars), + value) + ) + + message = ( + "Prohibited vars {} found in parameter values".format( + ", ".join(all_container_vars) + ) + ) + raise PublishValidationError(message, title=self.label) @classmethod def get_invalid(cls): @@ -63,7 +82,7 @@ class ValidateWorkfilePaths( def repair(cls, instance): invalid = cls.get_invalid() for param in invalid: - cls.log.info("processing: {}".format(param.path())) + cls.log.info("Processing: {}".format(param.path())) cls.log.info("Replacing {} for {}".format( param.unexpandedString(), hou.text.expandString(param.unexpandedString()))) diff --git a/openpype/hosts/maya/plugins/publish/collect_inputs.py b/openpype/hosts/maya/plugins/publish/collect_inputs.py index 9c3f0f5efa..895c92762b 100644 --- a/openpype/hosts/maya/plugins/publish/collect_inputs.py +++ b/openpype/hosts/maya/plugins/publish/collect_inputs.py @@ -166,7 +166,7 @@ class CollectUpstreamInputs(pyblish.api.InstancePlugin): inputs = [c["representation"] for c in containers] instance.data["inputRepresentations"] = inputs - self.log.info("Collected inputs: %s" % inputs) + self.log.debug("Collected inputs: %s" % inputs) def _collect_renderlayer_inputs(self, scene_containers, instance): """Collects inputs from nodes in renderlayer, incl. shaders + camera""" diff --git a/openpype/hosts/maya/plugins/publish/save_scene.py b/openpype/hosts/maya/plugins/publish/save_scene.py index 45e62e7b44..495c339731 100644 --- a/openpype/hosts/maya/plugins/publish/save_scene.py +++ b/openpype/hosts/maya/plugins/publish/save_scene.py @@ -31,5 +31,5 @@ class SaveCurrentScene(pyblish.api.ContextPlugin): # remove lockfile before saving if is_workfile_lock_enabled("maya", project_name, project_settings): remove_workfile_lock(current) - self.log.info("Saving current file..") + self.log.info("Saving current file: {}".format(current)) cmds.file(save=True, force=True) diff --git a/openpype/hosts/substancepainter/plugins/publish/save_workfile.py b/openpype/hosts/substancepainter/plugins/publish/save_workfile.py index 4874b5e5c7..9662f31922 100644 --- a/openpype/hosts/substancepainter/plugins/publish/save_workfile.py +++ b/openpype/hosts/substancepainter/plugins/publish/save_workfile.py @@ -16,11 +16,12 @@ class SaveCurrentWorkfile(pyblish.api.ContextPlugin): def process(self, context): host = registered_host() - if context.data["currentFile"] != host.get_current_workfile(): + current = host.get_current_workfile() + if context.data["currentFile"] != current: raise KnownPublishError("Workfile has changed during publishing!") if host.has_unsaved_changes(): - self.log.info("Saving current file..") + self.log.info("Saving current file: {}".format(current)) host.save_workfile() else: self.log.debug("Skipping workfile save because there are no " diff --git a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py index 717391100d..a48596c6bf 100644 --- a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py @@ -73,7 +73,7 @@ class FusionSubmitDeadline( def process(self, instance): if not instance.data.get("farm"): - self.log.info("Skipping local instance.") + self.log.debug("Skipping local instance.") return attribute_values = self.get_attr_values_from_data( diff --git a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py index 5c598df94b..4900231783 100644 --- a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py @@ -86,7 +86,7 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin, def process(self, instance): if not instance.data.get("farm"): - self.log.info("Skipping local instance.") + self.log.debug("Skipping local instance.") return instance.data["attributeValues"] = self.get_attr_values_from_data( diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index eeb813cb62..68eb0a437d 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -762,7 +762,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): """ if not instance.data.get("farm"): - self.log.info("Skipping local instance.") + self.log.debug("Skipping local instance.") return data = instance.data.copy() diff --git a/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py b/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py index 7c8ab62d4d..e1c0595830 100644 --- a/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py +++ b/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py @@ -26,7 +26,7 @@ class ValidateDeadlinePools(OptionalPyblishPluginMixin, def process(self, instance): if not instance.data.get("farm"): - self.log.info("Skipping local instance.") + self.log.debug("Skipping local instance.") return # get default deadline webservice url from deadline module diff --git a/openpype/pipeline/publish/publish_plugins.py b/openpype/pipeline/publish/publish_plugins.py index a38896ec8e..a67c8397b1 100644 --- a/openpype/pipeline/publish/publish_plugins.py +++ b/openpype/pipeline/publish/publish_plugins.py @@ -379,7 +379,9 @@ class ColormanagedPyblishPluginMixin(object): # check if ext in lower case is in self.allowed_ext if ext.lstrip(".").lower() not in self.allowed_ext: - self.log.debug("Extension is not in allowed extensions.") + self.log.debug( + "Extension '{}' is not in allowed extensions.".format(ext) + ) return if colorspace_settings is None: @@ -393,8 +395,7 @@ class ColormanagedPyblishPluginMixin(object): self.log.warning("No colorspace management was defined") return - self.log.info("Config data is : `{}`".format( - config_data)) + self.log.debug("Config data is: `{}`".format(config_data)) project_name = context.data["projectName"] host_name = context.data["hostName"] @@ -405,8 +406,7 @@ class ColormanagedPyblishPluginMixin(object): if isinstance(filename, list): filename = filename[0] - self.log.debug("__ filename: `{}`".format( - filename)) + self.log.debug("__ filename: `{}`".format(filename)) # get matching colorspace from rules colorspace = colorspace or get_imageio_colorspace_from_filepath( @@ -415,8 +415,7 @@ class ColormanagedPyblishPluginMixin(object): file_rules=file_rules, project_settings=project_settings ) - self.log.debug("__ colorspace: `{}`".format( - colorspace)) + self.log.debug("__ colorspace: `{}`".format(colorspace)) # infuse data to representation if colorspace: diff --git a/openpype/plugins/publish/cleanup.py b/openpype/plugins/publish/cleanup.py index b90c88890d..57cc9c0ab5 100644 --- a/openpype/plugins/publish/cleanup.py +++ b/openpype/plugins/publish/cleanup.py @@ -81,7 +81,8 @@ class CleanUp(pyblish.api.InstancePlugin): staging_dir = instance.data.get("stagingDir", None) if not staging_dir: - self.log.info("Staging dir not set.") + self.log.debug("Skipping cleanup. Staging dir not set " + "on instance: {}.".format(instance)) return if not os.path.normpath(staging_dir).startswith(temp_root): @@ -90,7 +91,7 @@ class CleanUp(pyblish.api.InstancePlugin): return if not os.path.exists(staging_dir): - self.log.info("No staging directory found: %s" % staging_dir) + self.log.debug("No staging directory found at: %s" % staging_dir) return if instance.data.get("stagingDir_persistent"): @@ -131,7 +132,9 @@ class CleanUp(pyblish.api.InstancePlugin): try: os.remove(src) except PermissionError: - self.log.warning("Insufficient permission to delete {}".format(src)) + self.log.warning( + "Insufficient permission to delete {}".format(src) + ) continue # add dir for cleanup diff --git a/openpype/plugins/publish/collect_anatomy_context_data.py b/openpype/plugins/publish/collect_anatomy_context_data.py index 55ce8e06f4..508b01447b 100644 --- a/openpype/plugins/publish/collect_anatomy_context_data.py +++ b/openpype/plugins/publish/collect_anatomy_context_data.py @@ -67,5 +67,6 @@ class CollectAnatomyContextData(pyblish.api.ContextPlugin): # Store context.data["anatomyData"] = anatomy_data - self.log.info("Global anatomy Data collected") - self.log.debug(json.dumps(anatomy_data, indent=4)) + self.log.debug("Global Anatomy Context Data collected:\n{}".format( + json.dumps(anatomy_data, indent=4) + )) diff --git a/openpype/plugins/publish/collect_anatomy_instance_data.py b/openpype/plugins/publish/collect_anatomy_instance_data.py index 4fbb93324b..128ad90b4f 100644 --- a/openpype/plugins/publish/collect_anatomy_instance_data.py +++ b/openpype/plugins/publish/collect_anatomy_instance_data.py @@ -46,17 +46,17 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): follow_workfile_version = False def process(self, context): - self.log.info("Collecting anatomy data for all instances.") + self.log.debug("Collecting anatomy data for all instances.") project_name = context.data["projectName"] self.fill_missing_asset_docs(context, project_name) self.fill_latest_versions(context, project_name) self.fill_anatomy_data(context) - self.log.info("Anatomy Data collection finished.") + self.log.debug("Anatomy Data collection finished.") def fill_missing_asset_docs(self, context, project_name): - self.log.debug("Qeurying asset documents for instances.") + self.log.debug("Querying asset documents for instances.") context_asset_doc = context.data.get("assetEntity") @@ -271,7 +271,7 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): instance_name = instance.data["name"] instance_label = instance.data.get("label") if instance_label: - instance_name += "({})".format(instance_label) + instance_name += " ({})".format(instance_label) self.log.debug("Anatomy data for instance {}: {}".format( instance_name, json.dumps(anatomy_data, indent=4) diff --git a/openpype/plugins/publish/collect_anatomy_object.py b/openpype/plugins/publish/collect_anatomy_object.py index 725cae2b14..f792cf3abd 100644 --- a/openpype/plugins/publish/collect_anatomy_object.py +++ b/openpype/plugins/publish/collect_anatomy_object.py @@ -30,6 +30,6 @@ class CollectAnatomyObject(pyblish.api.ContextPlugin): context.data["anatomy"] = Anatomy(project_name) - self.log.info( + self.log.debug( "Anatomy object collected for project \"{}\".".format(project_name) ) diff --git a/openpype/plugins/publish/collect_custom_staging_dir.py b/openpype/plugins/publish/collect_custom_staging_dir.py index b749b251c0..669c4873e0 100644 --- a/openpype/plugins/publish/collect_custom_staging_dir.py +++ b/openpype/plugins/publish/collect_custom_staging_dir.py @@ -65,6 +65,6 @@ class CollectCustomStagingDir(pyblish.api.InstancePlugin): else: result_str = "Not adding" - self.log.info("{} custom staging dir for instance with '{}'".format( + self.log.debug("{} custom staging dir for instance with '{}'".format( result_str, family )) diff --git a/openpype/plugins/publish/collect_from_create_context.py b/openpype/plugins/publish/collect_from_create_context.py index 5fcf8feb56..4888476fff 100644 --- a/openpype/plugins/publish/collect_from_create_context.py +++ b/openpype/plugins/publish/collect_from_create_context.py @@ -92,5 +92,5 @@ class CollectFromCreateContext(pyblish.api.ContextPlugin): instance.data["transientData"] = transient_data - self.log.info("collected instance: {}".format(instance.data)) - self.log.info("parsing data: {}".format(in_data)) + self.log.debug("collected instance: {}".format(instance.data)) + self.log.debug("parsing data: {}".format(in_data)) diff --git a/openpype/plugins/publish/collect_scene_version.py b/openpype/plugins/publish/collect_scene_version.py index fdbcb3cb9d..cd3231a07d 100644 --- a/openpype/plugins/publish/collect_scene_version.py +++ b/openpype/plugins/publish/collect_scene_version.py @@ -48,10 +48,13 @@ class CollectSceneVersion(pyblish.api.ContextPlugin): if '' in filename: return + self.log.debug( + "Collecting scene version from filename: {}".format(filename) + ) + version = get_version_from_path(filename) assert version, "Cannot determine version" rootVersion = int(version) context.data['version'] = rootVersion - self.log.info("{}".format(type(rootVersion))) self.log.info('Scene Version: %s' % context.data.get('version')) diff --git a/openpype/plugins/publish/extract_burnin.py b/openpype/plugins/publish/extract_burnin.py index a12e8d18b4..10b366dcd6 100644 --- a/openpype/plugins/publish/extract_burnin.py +++ b/openpype/plugins/publish/extract_burnin.py @@ -517,8 +517,8 @@ class ExtractBurnin(publish.Extractor): """ if "burnin" not in (repre.get("tags") or []): - self.log.info(( - "Representation \"{}\" don't have \"burnin\" tag. Skipped." + self.log.debug(( + "Representation \"{}\" does not have \"burnin\" tag. Skipped." ).format(repre["name"])) return False diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 58e0350a2e..45b10620d1 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -336,13 +336,13 @@ class ExtractOIIOTranscode(publish.Extractor): if repre.get("ext") not in self.supported_exts: self.log.debug(( - "Representation '{}' of unsupported extension. Skipped." - ).format(repre["name"])) + "Representation '{}' has unsupported extension: '{}'. Skipped." + ).format(repre["name"], repre.get("ext"))) return False if not repre.get("files"): self.log.debug(( - "Representation '{}' have empty files. Skipped." + "Representation '{}' has empty files. Skipped." ).format(repre["name"])) return False diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index 1062683319..a68addda7d 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -92,8 +92,8 @@ class ExtractReview(pyblish.api.InstancePlugin): host_name = instance.context.data["hostName"] family = self.main_family_from_instance(instance) - self.log.info("Host: \"{}\"".format(host_name)) - self.log.info("Family: \"{}\"".format(family)) + self.log.debug("Host: \"{}\"".format(host_name)) + self.log.debug("Family: \"{}\"".format(family)) profile = filter_profiles( self.profiles, @@ -351,7 +351,7 @@ class ExtractReview(pyblish.api.InstancePlugin): temp_data = self.prepare_temp_data(instance, repre, output_def) files_to_clean = [] if temp_data["input_is_sequence"]: - self.log.info("Filling gaps in sequence.") + self.log.debug("Checking sequence to fill gaps in sequence..") files_to_clean = self.fill_sequence_gaps( files=temp_data["origin_repre"]["files"], staging_dir=new_repre["stagingDir"], diff --git a/openpype/plugins/publish/extract_thumbnail.py b/openpype/plugins/publish/extract_thumbnail.py index 54b933a76d..b98ab64f56 100644 --- a/openpype/plugins/publish/extract_thumbnail.py +++ b/openpype/plugins/publish/extract_thumbnail.py @@ -36,7 +36,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): ).format(subset_name)) return - self.log.info( + self.log.debug( "Processing instance with subset name {}".format(subset_name) ) @@ -89,13 +89,13 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): src_staging = os.path.normpath(repre["stagingDir"]) full_input_path = os.path.join(src_staging, input_file) - self.log.info("input {}".format(full_input_path)) + self.log.debug("input {}".format(full_input_path)) filename = os.path.splitext(input_file)[0] jpeg_file = filename + "_thumb.jpg" full_output_path = os.path.join(dst_staging, jpeg_file) if oiio_supported: - self.log.info("Trying to convert with OIIO") + self.log.debug("Trying to convert with OIIO") # If the input can read by OIIO then use OIIO method for # conversion otherwise use ffmpeg thumbnail_created = self.create_thumbnail_oiio( @@ -148,7 +148,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): def _already_has_thumbnail(self, repres): for repre in repres: - self.log.info("repre {}".format(repre)) + self.log.debug("repre {}".format(repre)) if repre["name"] == "thumbnail": return True return False @@ -173,20 +173,20 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): return filtered_repres def create_thumbnail_oiio(self, src_path, dst_path): - self.log.info("outputting {}".format(dst_path)) + self.log.info("Extracting thumbnail {}".format(dst_path)) oiio_tool_path = get_oiio_tools_path() oiio_cmd = [ oiio_tool_path, "-a", src_path, "-o", dst_path ] - self.log.info("running: {}".format(" ".join(oiio_cmd))) + self.log.debug("running: {}".format(" ".join(oiio_cmd))) try: run_subprocess(oiio_cmd, logger=self.log) return True except Exception: self.log.warning( - "Failed to create thubmnail using oiiotool", + "Failed to create thumbnail using oiiotool", exc_info=True ) return False diff --git a/openpype/plugins/publish/extract_thumbnail_from_source.py b/openpype/plugins/publish/extract_thumbnail_from_source.py index a92f762cde..a9c95d6065 100644 --- a/openpype/plugins/publish/extract_thumbnail_from_source.py +++ b/openpype/plugins/publish/extract_thumbnail_from_source.py @@ -39,7 +39,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): self._create_context_thumbnail(instance.context) subset_name = instance.data["subset"] - self.log.info( + self.log.debug( "Processing instance with subset name {}".format(subset_name) ) thumbnail_source = instance.data.get("thumbnailSource") @@ -104,7 +104,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): full_output_path = os.path.join(dst_staging, dst_filename) if oiio_supported: - self.log.info("Trying to convert with OIIO") + self.log.debug("Trying to convert with OIIO") # If the input can read by OIIO then use OIIO method for # conversion otherwise use ffmpeg thumbnail_created = self.create_thumbnail_oiio( diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 8e984a9e97..f392cf67f7 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -267,7 +267,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): instance_stagingdir = instance.data.get("stagingDir") if not instance_stagingdir: - self.log.info(( + self.log.debug(( "{0} is missing reference to staging directory." " Will try to get it from representation." ).format(instance)) @@ -480,7 +480,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): update_data ) - self.log.info("Prepared subset: {}".format(subset_name)) + self.log.debug("Prepared subset: {}".format(subset_name)) return subset_doc def prepare_version(self, instance, op_session, subset_doc, project_name): @@ -521,7 +521,9 @@ class IntegrateAsset(pyblish.api.InstancePlugin): project_name, version_doc["type"], version_doc ) - self.log.info("Prepared version: v{0:03d}".format(version_doc["name"])) + self.log.debug( + "Prepared version: v{0:03d}".format(version_doc["name"]) + ) return version_doc diff --git a/openpype/plugins/publish/integrate_legacy.py b/openpype/plugins/publish/integrate_legacy.py index c67ce62bf6..c238cca633 100644 --- a/openpype/plugins/publish/integrate_legacy.py +++ b/openpype/plugins/publish/integrate_legacy.py @@ -147,7 +147,9 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): def process(self, instance): if instance.data.get("processedWithNewIntegrator"): - self.log.info("Instance was already processed with new integrator") + self.log.debug( + "Instance was already processed with new integrator" + ) return for ef in self.exclude_families: @@ -274,7 +276,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): stagingdir = instance.data.get("stagingDir") if not stagingdir: - self.log.info(( + self.log.debug(( "{0} is missing reference to staging directory." " Will try to get it from representation." ).format(instance)) diff --git a/openpype/plugins/publish/integrate_thumbnail.py b/openpype/plugins/publish/integrate_thumbnail.py index 16cc47d432..f6d4f654f5 100644 --- a/openpype/plugins/publish/integrate_thumbnail.py +++ b/openpype/plugins/publish/integrate_thumbnail.py @@ -41,7 +41,7 @@ class IntegrateThumbnails(pyblish.api.ContextPlugin): # Filter instances which can be used for integration filtered_instance_items = self._prepare_instances(context) if not filtered_instance_items: - self.log.info( + self.log.debug( "All instances were filtered. Thumbnail integration skipped." ) return @@ -162,7 +162,7 @@ class IntegrateThumbnails(pyblish.api.ContextPlugin): # Skip instance if thumbnail path is not available for it if not thumbnail_path: - self.log.info(( + self.log.debug(( "Skipping thumbnail integration for instance \"{}\"." " Instance and context" " thumbnail paths are not available." From 22e7f9bd8497bdda99eea9e560788fdea35cb21d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Wed, 24 May 2023 10:58:40 +0200 Subject: [PATCH 123/189] Update openpype/hosts/nuke/plugins/publish/extract_thumbnail.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/nuke/plugins/publish/extract_thumbnail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index 2336487b37..34f4b4e8cf 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -125,7 +125,7 @@ class ExtractThumbnail(publish.Extractor): temporary_nodes.append(rnode) previous_node = rnode - if not self.reposition_nodes: + if self.reposition_nodes is None: # [deprecated] create reformat node old way reformat_node = nuke.createNode("Reformat") ref_node = self.nodes.get("Reformat", None) From 084a15ec8c2433e2584dd2c0646417cf811262cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Wed, 24 May 2023 10:58:47 +0200 Subject: [PATCH 124/189] Update openpype/hosts/nuke/plugins/publish/extract_thumbnail.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/nuke/plugins/publish/extract_thumbnail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index 34f4b4e8cf..21eefda249 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -30,7 +30,7 @@ class ExtractThumbnail(publish.Extractor): bake_viewer_process = True bake_viewer_input_process = True nodes = {} - reposition_nodes = [] + reposition_nodes = None def process(self, instance): if instance.data.get("farm"): From e5733450e428f7f26e5bfe76fc9fe1e80b42b9f2 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 24 May 2023 12:18:57 +0200 Subject: [PATCH 125/189] Global: plugins cleanup plugin will leave beauty rendered files (#4790) * OP-1066 - add expected files in Deadline into explicit cleanup Implicit cleanup doesn't work correctly, safest option is for DL submissions to mark only files that should be rendered to be deleted after successful publish. * OP-1066 - moved collecting of expected files into collector Parsing of json didn't have context implemented, it is easier to mark expected files in collector. * OP-4793 - delete full stagingDir Reviews might be extracted into staging dir, should be removed too. * Revert "OP-4793 - delete full stagingDir" This reverts commit 8b002191e1ad3b31a0cbe439ca1158946c43b049. * OP-1066 - added function to mark representation files to be cleaned up Should be applicable for all new representations, as reviews, thumbnails, to clean up their intermediate files. * OP-1066 - moved files to better file Cleaned up occurences where not necessary. * OP-1066 - removed unused import * OP-1066 - removed unnecessary setdefault * OP-1066 - removed unnecessary logging * OP-1066 - cleanup metadata json Try to cleanup parent folder if empty. --- openpype/pipeline/publish/lib.py | 19 +++++++++++++++++++ .../plugins/publish/collect_rendered_files.py | 6 ++++++ openpype/plugins/publish/extract_burnin.py | 3 +++ openpype/plugins/publish/extract_review.py | 3 +++ 4 files changed, 31 insertions(+) diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index 63a856e326..b55f813b5e 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -847,3 +847,22 @@ def _validate_transient_template(project_name, template_name, anatomy): raise ValueError(("There is not set \"folder\" template in \"{}\" anatomy" # noqa " for project \"{}\"." ).format(template_name, project_name)) + + +def add_repre_files_for_cleanup(instance, repre): + """ Explicitly mark repre files to be deleted. + + Should be used on intermediate files (eg. review, thumbnails) to be + explicitly deleted. + """ + files = repre["files"] + staging_dir = repre.get("stagingDir") + if not staging_dir: + return + + if isinstance(files, str): + files = [files] + + for file_name in files: + expected_file = os.path.join(staging_dir, file_name) + instance.context.data["cleanupFullPaths"].append(expected_file) diff --git a/openpype/plugins/publish/collect_rendered_files.py b/openpype/plugins/publish/collect_rendered_files.py index 8f8d0a5eeb..6c8d1e9ca5 100644 --- a/openpype/plugins/publish/collect_rendered_files.py +++ b/openpype/plugins/publish/collect_rendered_files.py @@ -13,6 +13,7 @@ import json import pyblish.api from openpype.pipeline import legacy_io, KnownPublishError +from openpype.pipeline.publish.lib import add_repre_files_for_cleanup class CollectRenderedFiles(pyblish.api.ContextPlugin): @@ -89,6 +90,7 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin): # now we can just add instances from json file and we are done for instance_data in data.get("instances"): + self.log.info(" - processing instance for {}".format( instance_data.get("subset"))) instance = self._context.create_instance( @@ -107,6 +109,8 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin): self._fill_staging_dir(repre_data, anatomy) representations.append(repre_data) + add_repre_files_for_cleanup(instance, repre_data) + instance.data["representations"] = representations # add audio if in metadata data @@ -157,6 +161,8 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin): os.environ.update(session_data) session_is_set = True self._process_path(data, anatomy) + context.data["cleanupFullPaths"].append(path) + context.data["cleanupEmptyDirs"].append(os.path.dirname(path)) except Exception as e: self.log.error(e, exc_info=True) raise Exception("Error") from e diff --git a/openpype/plugins/publish/extract_burnin.py b/openpype/plugins/publish/extract_burnin.py index 10b366dcd6..6a8ae958d2 100644 --- a/openpype/plugins/publish/extract_burnin.py +++ b/openpype/plugins/publish/extract_burnin.py @@ -19,6 +19,7 @@ from openpype.lib import ( should_convert_for_ffmpeg ) from openpype.lib.profiles_filtering import filter_profiles +from openpype.pipeline.publish.lib import add_repre_files_for_cleanup class ExtractBurnin(publish.Extractor): @@ -353,6 +354,8 @@ class ExtractBurnin(publish.Extractor): # Add new representation to instance instance.data["representations"].append(new_repre) + add_repre_files_for_cleanup(instance, new_repre) + # Cleanup temp staging dir after procesisng of output definitions if do_convert: temp_dir = repre["stagingDir"] diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index a68addda7d..fa58c03df1 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -24,6 +24,7 @@ from openpype.lib.transcoding import ( get_transcode_temp_directory, ) from openpype.pipeline.publish import KnownPublishError +from openpype.pipeline.publish.lib import add_repre_files_for_cleanup class ExtractReview(pyblish.api.InstancePlugin): @@ -425,6 +426,8 @@ class ExtractReview(pyblish.api.InstancePlugin): ) instance.data["representations"].append(new_repre) + add_repre_files_for_cleanup(instance, new_repre) + def input_is_sequence(self, repre): """Deduce from representation data if input is sequence.""" # TODO GLOBAL ISSUE - Find better way how to find out if input From 8410055b2499e30c80cc7d3bd8c4d10cf76369bb Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 24 May 2023 10:21:38 +0000 Subject: [PATCH 126/189] [Automated] Release --- CHANGELOG.md | 298 ++++++++++++++++++++++++++++++++++++++++++++ openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 300 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bba6b64bfe..a33904735b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,304 @@ # Changelog +## [3.15.8](https://github.com/ynput/OpenPype/tree/3.15.8) + + +[Full Changelog](https://github.com/ynput/OpenPype/compare/3.15.7...3.15.8) + +### **πŸ†• New features** + + +

+Publisher: Show instances in report page #4915 + +Show publish instances in report page. Also added basic log view with logs grouped by instance. Validation error detail now have 2 colums, one with erro details second with logs. Crashed state shows fast access to report action buttons. Success will show only logs. Publish frame is shrunked automatically on publish stop. + + +___ + +
+ + +
+Fusion - Loader plugins updates #4920 + +Update to some Fusion loader plugins:The sequence loader can now load footage from the image and online family.The FBX loader can now import all formats Fusions FBX node can read.You can now import the content of another workfile into your current comp with the workfile loader. + + +___ + +
+ + +
+Fusion: deadline farm rendering #4955 + +Enabling Fusion for deadline farm rendering. + + +___ + +
+ + +
+AfterEffects: set frame range and resolution #4983 + +Frame information (frame start, duration, fps) and resolution (width and height) is applied to selected composition from Asset Management System (Ftrack or DB) automatically when published instance is created.It is also possible explicitly propagate both values from DB to selected composition by newly added menu buttons. + + +___ + +
+ + +
+Publish: Enhance automated publish plugin settings #4986 + +Added plugins option to define settings category where to look for settings of a plugin and added public helper functions to apply settings `get_plugin_settings` and `apply_plugin_settings_automatically`. + + +___ + +
+ +### **πŸš€ Enhancements** + + +
+Load Rig References - Change Rig to Animation in Animation instance #4877 + +We are using the template builder to build an animation scene. All the rig placeholders are imported correctly, but the automatically created animation instances retain the rig family in their names and subsets. In our example, we need animationMain instead of rigMain, because this name will be used in the following steps like lighting.Here is the result we need. I checked, and it's not a template builder problem, because even if I load a rig as a reference, the result is the same. For me, since we are in the animation instance, it makes more sense to have animation instead of rig in the name. The naming is just fine if we use create from the Openpype menu. + + +___ + +
+ + +
+Enhancement: Resolve prelaunch code refactoring and update defaults #4916 + +The main reason of this PR is wrong default settings in `openpype/settings/defaults/system_settings/applications.json` for Resolve host. The `bin` folder should not be a part of the macos and Linux `RESOLVE_PYTHON3_PATH` variable.The rest of this PR is some code cleanups for Resolve prelaunch hook to simplify further development.Also added a .gitignore for vscode workspace files. + + +___ + +
+ + +
+Unreal: 🚚 move Unreal plugin to separate repository #4980 + +To support Epic Marketplace have to move AYON Unreal integration plugins to separate repository. This is replacing current files with git submodule, so the change should be functionally without impact.New repository lives here: https://github.com/ynput/ayon-unreal-plugin + + +___ + +
+ + +
+General: Lib code cleanup #5003 + +Small cleanup in lib files in openpype. + + +___ + +
+ + +
+Allow to open with djv by extension instead of representation name #5004 + +Filter open in djv action by extension instead of representation. + + +___ + +
+ + +
+DJV open action `extensions` as `set` #5005 + +Change `extensions` attribute to `set`. + + +___ + +
+ + +
+Nuke: extract thumbnail with multiple reposition nodes #5011 + +Added support for multiple reposition nodes. + + +___ + +
+ + +
+Enhancement: Improve logging levels and messages for artist facing publish reports #5018 + +Tweak the logging levels and messages to try and only show those logs that an artist should see and could understand. Move anything that's slightly more involved into a "debug" message instead. + + +___ + +
+ +### **πŸ› Bug fixes** + + +
+Bugfix/frame variable fix #4978 + +Renamed variables to match OpenPype terminology to reduce confusion and add consistency. +___ + +
+ + +
+Global: plugins cleanup plugin will leave beauty rendered files #4790 + +Attempt to mark more files to be cleaned up explicitly in intermediate `renders` folder in work area for farm jobs. + + +___ + +
+ + +
+Fix: Download last workfile doesn't work if not already downloaded #4942 + +Some optimization condition is messing with the feature: if the published workfile is not already downloaded, it won't download it... + + +___ + +
+ + +
+Unreal: Fix transform when loading layout to match existing assets #4972 + +Fixed transform when loading layout to match existing assets. + + +___ + +
+ + +
+fix the bug of fbx loaders in Max #4977 + +bug fix of fbx loaders for not being able to parent to the CON instances while importing cameras(and models) which is published from other DCCs such as Maya. + + +___ + +
+ + +
+AfterEffects: allow returning stub with not saved workfile #4984 + +Allows to use Workfile app to Save first empty workfile. + + +___ + +
+ + +
+Blender: Fix Alembic loading #4985 + +Fixed problem occurring when trying to load an Alembic model in Blender. + + +___ + +
+ + +
+Unreal: Addon Py2 compatibility #4994 + +Fixed Python 2 compatibility of unreal addon. + + +___ + +
+ + +
+Nuke: fixed missing files key in representation #4999 + +Issue with missing keys once rendering target set to existing frames is fixed. Instance has to be evaluated in validation for missing files. + + +___ + +
+ + +
+Unreal: Fix the frame range when loading camera #5002 + +The keyframes of the camera, when loaded, were not using the correct frame range. + + +___ + +
+ + +
+Fusion: fixing frame range targeting #5013 + +Frame range targeting at Rendering instances is now following configured options. + + +___ + +
+ + +
+Deadline: fix selection from multiple webservices #5015 + +Multiple different DL webservice could be configured. First they must by configured in System Settings., then they could be configured per project in `project_settings/deadline/deadline_servers`.Only single webservice could be a target of publish though. + + +___ + +
+ +### **Merged pull requests** + + +
+3dsmax: Refactored publish plugins to use proper implementation of pymxs #4988 + + +___ + +
+ + + + ## [3.15.7](https://github.com/ynput/OpenPype/tree/3.15.7) diff --git a/openpype/version.py b/openpype/version.py index 3d7f64b991..342bbfc85a 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.8-nightly.3" +__version__ = "3.15.8" diff --git a/pyproject.toml b/pyproject.toml index 190ecb9329..a72a3d66d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.15.7" # OpenPype +version = "3.15.8" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 4647e39142da5c0a1a5cc62844a38c105c166dc9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 24 May 2023 10:22:35 +0000 Subject: [PATCH 127/189] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index a9f1f1cc02..4d7d06a2c8 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.15.8 - 3.15.8-nightly.3 - 3.15.8-nightly.2 - 3.15.8-nightly.1 @@ -134,7 +135,6 @@ body: - 3.14.2-nightly.4 - 3.14.2-nightly.3 - 3.14.2-nightly.2 - - 3.14.2-nightly.1 validations: required: true - type: dropdown From b5827e8cdfcde48a7a9eff6aa29a21f48541ab66 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 24 May 2023 12:18:05 +0100 Subject: [PATCH 128/189] Fix section range on update camera --- .../hosts/unreal/plugins/load/load_camera.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index d198be29f4..c4fe9df70b 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -387,6 +387,30 @@ class CameraLoader(plugin.Loader): str(representation["data"]["path"]) ) + # Set range of all sections + # Changing the range of the section is not enough. We need to change + # the frame of all the keys in the section. + project_name = legacy_io.active_project() + asset = container.get('asset') + data = get_asset_by_name(project_name, asset)["data"] + + for possessable in new_sequence.get_possessables(): + for tracks in possessable.get_tracks(): + for section in tracks.get_sections(): + section.set_range( + data.get('clipIn'), + data.get('clipOut') + 1) + for channel in section.get_all_channels(): + for key in channel.get_keys(): + old_time = key.get_time().get_editor_property( + 'frame_number') + old_time_value = old_time.get_editor_property( + 'value') + new_time = old_time_value + ( + data.get('clipIn') - data.get('frameStart') + ) + key.set_time(unreal.FrameNumber(value=new_time)) + data = { "representation": str(representation["_id"]), "parent": str(representation["parent"]) From 7cfbb972a5ad41eea6e7bac6775ec8697edcb342 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 24 May 2023 12:40:28 +0100 Subject: [PATCH 129/189] Fix sequence frames validator to use correct data --- .../hosts/unreal/plugins/publish/validate_sequence_frames.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py b/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py index e6584e130f..76bb25fac3 100644 --- a/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py +++ b/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py @@ -31,8 +31,8 @@ class ValidateSequenceFrames(pyblish.api.InstancePlugin): frames = list(collection.indexes) current_range = (frames[0], frames[-1]) - required_range = (data["frameStart"], - data["frameEnd"]) + required_range = (data["clipIn"], + data["clipOut"]) if current_range != required_range: raise ValueError(f"Invalid frame range: {current_range} - " From 7e692ad5acc284540f1d29672ea86386b0f22468 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 24 May 2023 14:01:50 +0200 Subject: [PATCH 130/189] added option to nest settings templates --- openpype/settings/entities/lib.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index 1c7dc9bed0..93abc27b0e 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -323,7 +323,10 @@ class SchemasHub: filled_template = self._fill_template( schema_data, template_def ) - return filled_template + new_template_def = [] + for item in filled_template: + new_template_def.extend(self.resolve_schema_data(item)) + return new_template_def def create_schema_object(self, schema_data, *args, **kwargs): """Create entity for passed schema data. From 45e1dbc8410fb66dd7e685e77946db2952b35672 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 24 May 2023 13:14:46 +0100 Subject: [PATCH 131/189] Fix render instances collection to use correct data --- .../hosts/unreal/plugins/publish/collect_render_instances.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/unreal/plugins/publish/collect_render_instances.py b/openpype/hosts/unreal/plugins/publish/collect_render_instances.py index a352b2c3f3..dad0310dfc 100644 --- a/openpype/hosts/unreal/plugins/publish/collect_render_instances.py +++ b/openpype/hosts/unreal/plugins/publish/collect_render_instances.py @@ -103,8 +103,8 @@ class CollectRenderInstances(pyblish.api.InstancePlugin): new_instance.data["representations"] = [] repr = { - 'frameStart': s.get('frame_range')[0], - 'frameEnd': s.get('frame_range')[1], + 'frameStart': instance.data["frameStart"], + 'frameEnd': instance.data["frameEnd"], 'name': 'png', 'ext': 'png', 'files': frames, From d4212ef9918e805025fb93fdfdaf5b5fa82f2d7c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 24 May 2023 22:04:42 +0200 Subject: [PATCH 132/189] Return any timeline in case none is detected as active also adding in host test --- openpype/hosts/resolve/api/lib.py | 16 +++++++++------- .../utility_scripts/tests/testing_timeline_op.py | 13 +++++++++++++ 2 files changed, 22 insertions(+), 7 deletions(-) create mode 100644 openpype/hosts/resolve/utility_scripts/tests/testing_timeline_op.py diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index 1c33749a77..d42521200a 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -91,16 +91,16 @@ def get_current_project(): return self.project_manager.GetCurrentProject() -def get_current_timeline(any=False, new=False): +def get_current_timeline(new=False, get_any=False): """Get current timeline object. Args: - any (bool, optional): return any even new if no timeline available. - Defaults to False. new (bool, optional): return only new timeline. Defaults to False. + get_any (bool, optional): return any even new if no timeline available. + Defaults to False. Returns: - _type_: _description_ + object: resolve.Timeline """ # get current project project = get_current_project() @@ -111,12 +111,14 @@ def get_current_timeline(any=False, new=False): if timeline and not new: return timeline - # if any is True then return any timeline - if any: + # if get_any is True then return any timeline + if get_any: timeline_count = project.GetTimelineCount() if timeline_count == 0: # if there is no timeline then create a new one new = True + else: + return project.GetTimelineByIndex(1) # create new timeline if new is True if new: @@ -336,7 +338,7 @@ def get_current_timeline_items( selecting_color = selecting_color or "Chocolate" project = get_current_project() # make sure some timeline will be active with `any` argument - timeline = get_current_timeline(any=True) + timeline = get_current_timeline(get_any=True) selected_clips = [] # get all tracks count filtered by track type diff --git a/openpype/hosts/resolve/utility_scripts/tests/testing_timeline_op.py b/openpype/hosts/resolve/utility_scripts/tests/testing_timeline_op.py new file mode 100644 index 0000000000..8270496f64 --- /dev/null +++ b/openpype/hosts/resolve/utility_scripts/tests/testing_timeline_op.py @@ -0,0 +1,13 @@ +#! python3 +from openpype.pipeline import install_host +from openpype.hosts.resolve import api as bmdvr +from openpype.hosts.resolve.api.lib import get_current_project + +if __name__ == "__main__": + install_host(bmdvr) + project = get_current_project() + timeline_count = project.GetTimelineCount() + print(f"Timeline count: {timeline_count}") + timeline = project.GetTimelineByIndex(timeline_count) + print(f"Timeline name: {timeline.GetName()}") + print(timeline.GetTrackCount("video")) From 99a1be366e77db5549b294b8e37bb3089061cdd4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 24 May 2023 22:19:46 +0200 Subject: [PATCH 133/189] nuke: callback for dirmapping is on demand --- openpype/hosts/nuke/api/pipeline.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index d649ffae7f..75b0f80d21 100644 --- a/openpype/hosts/nuke/api/pipeline.py +++ b/openpype/hosts/nuke/api/pipeline.py @@ -151,6 +151,7 @@ class NukeHost( def add_nuke_callbacks(): """ Adding all available nuke callbacks """ + nuke_settings = get_current_project_settings()["nuke"] workfile_settings = WorkfileSettings() # Set context settings. nuke.addOnCreate( @@ -169,7 +170,10 @@ def add_nuke_callbacks(): # # set apply all workfile settings on script load and save nuke.addOnScriptLoad(WorkfileSettings().set_context_settings) - nuke.addFilenameFilter(dirmap_file_name_filter) + if nuke_settings["nuke-dirmap"]["enabled"]: + log.info("Added Nuke's dirmaping callback ...") + # Add dirmap for file paths. + nuke.addFilenameFilter(dirmap_file_name_filter) log.info("Added Nuke callbacks ...") From 41ae41d7512ebdc3270d49904f264cdde6c44f75 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 25 May 2023 10:00:29 +0200 Subject: [PATCH 134/189] Enhancement/publisher: Remove "hit play to continue" label on continue (#5029) * Clear message label on publish so that on "continue" it does not persist the "Hit play to continue" message * Clear main label on reset since the label isn't visible anyway * Remove unnecessary comment --------- Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/tools/publisher/widgets/publish_frame.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/tools/publisher/widgets/publish_frame.py b/openpype/tools/publisher/widgets/publish_frame.py index d21130deff..d423f97047 100644 --- a/openpype/tools/publisher/widgets/publish_frame.py +++ b/openpype/tools/publisher/widgets/publish_frame.py @@ -310,7 +310,7 @@ class PublishFrame(QtWidgets.QWidget): self._set_success_property() self._set_progress_visibility(True) - self._main_label.setText("Hit publish (play button)! If you want") + self._main_label.setText("") self._message_label_top.setText("") self._reset_btn.setEnabled(True) @@ -331,6 +331,7 @@ class PublishFrame(QtWidgets.QWidget): self._set_success_property(3) self._set_progress_visibility(True) self._set_main_label("Publishing...") + self._message_label_top.setText("") self._reset_btn.setEnabled(False) self._stop_btn.setEnabled(True) From 55040e6f74116b23531fa87f8965b83ab68d1316 Mon Sep 17 00:00:00 2001 From: Thomas Fricard <51854004+friquette@users.noreply.github.com> Date: Thu, 25 May 2023 10:32:36 +0200 Subject: [PATCH 135/189] Drop-down menu to list all families in create placeholder (#4928) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * convert family text field to enum field * get families from loaders and not creators * refactor the list families part * remove discover_loader_plugins call since there is already a variable with loaders plugins --------- Co-authored-by: Thomas Fricard Co-authored-by: ClΓ©ment Hector --- .../workfile/workfile_template_builder.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index a3d7340367..896ed40f2d 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -43,6 +43,7 @@ from openpype.pipeline.load import ( get_contexts_for_repre_docs, load_with_repre_context, ) + from openpype.pipeline.create import ( discover_legacy_creator_plugins, CreateContext, @@ -1246,6 +1247,16 @@ class PlaceholderLoadMixin(object): loader_items = list(sorted(loader_items, key=lambda i: i["label"])) options = options or {} + + # Get families from all loaders excluding "*" + families = set() + for loader in loaders_by_name.values(): + families.update(loader.families) + families.discard("*") + + # Sort for readability + families = list(sorted(families)) + return [ attribute_definitions.UISeparatorDef(), attribute_definitions.UILabelDef("Main attributes"), @@ -1272,11 +1283,11 @@ class PlaceholderLoadMixin(object): " field \"inputLinks\"" ) ), - attribute_definitions.TextDef( + attribute_definitions.EnumDef( "family", label="Family", default=options.get("family"), - placeholder="model, look, ..." + items=families ), attribute_definitions.TextDef( "representation", From 124493affd3e3b34563e35c83e34c7c66b5ee99b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 25 May 2023 11:51:44 +0200 Subject: [PATCH 136/189] Publisher: UI works with instances without label (#5032) * implemented helper function to get instance label * use 'get_publish_instance_label' in some of existing plugins * use 'get_publish_instance_label' in publisher controller --- openpype/pipeline/publish/__init__.py | 2 ++ openpype/pipeline/publish/lib.py | 32 +++++++++++++++++++ openpype/plugins/publish/extract_review.py | 16 +++------- .../plugins/publish/integrate_thumbnail.py | 12 ++----- openpype/tools/publisher/control.py | 3 +- 5 files changed, 44 insertions(+), 21 deletions(-) diff --git a/openpype/pipeline/publish/__init__.py b/openpype/pipeline/publish/__init__.py index 72f3774e1a..0c57915c05 100644 --- a/openpype/pipeline/publish/__init__.py +++ b/openpype/pipeline/publish/__init__.py @@ -39,6 +39,7 @@ from .lib import ( apply_plugin_settings_automatically, get_plugin_settings, + get_publish_instance_label, ) from .abstract_expected_files import ExpectedFiles @@ -85,6 +86,7 @@ __all__ = ( "apply_plugin_settings_automatically", "get_plugin_settings", + "get_publish_instance_label", "ExpectedFiles", diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index b55f813b5e..e87b865dce 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -30,6 +30,8 @@ from .contants import ( TRANSIENT_DIR_TEMPLATE ) +_ARG_PLACEHOLDER = object() + def get_template_name_profiles( project_name, project_settings=None, logger=None @@ -866,3 +868,33 @@ def add_repre_files_for_cleanup(instance, repre): for file_name in files: expected_file = os.path.join(staging_dir, file_name) instance.context.data["cleanupFullPaths"].append(expected_file) + + +def get_publish_instance_label(instance, default=_ARG_PLACEHOLDER): + """Try to get label from pyblish instance. + + First are checked 'label' and 'name' keys in instance data. If are not set + a default value is returned. Instance object is converted to string + if default value is not specific. + + Todos: + Maybe 'subset' key could be used too. + + Args: + instance (pyblish.api.Instance): Pyblish instance. + default (Optional[Any]): Default value to return if any + + Returns: + Union[Any]: Instance label or default label. + """ + + label = ( + instance.data.get("label") + or instance.data.get("name") + ) + if label: + return label + + if default is _ARG_PLACEHOLDER: + return str(instance) + return default diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index fa58c03df1..d04893fa7e 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -23,7 +23,10 @@ from openpype.lib.transcoding import ( convert_input_paths_for_ffmpeg, get_transcode_temp_directory, ) -from openpype.pipeline.publish import KnownPublishError +from openpype.pipeline.publish import ( + KnownPublishError, + get_publish_instance_label, +) from openpype.pipeline.publish.lib import add_repre_files_for_cleanup @@ -203,17 +206,8 @@ class ExtractReview(pyblish.api.InstancePlugin): return filtered_defs - @staticmethod - def get_instance_label(instance): - return ( - getattr(instance, "label", None) - or instance.data.get("label") - or instance.data.get("name") - or str(instance) - ) - def main_process(self, instance): - instance_label = self.get_instance_label(instance) + instance_label = get_publish_instance_label(instance) self.log.debug("Processing instance \"{}\"".format(instance_label)) profile_outputs = self._get_outputs_for_instance(instance) if not profile_outputs: diff --git a/openpype/plugins/publish/integrate_thumbnail.py b/openpype/plugins/publish/integrate_thumbnail.py index f6d4f654f5..2e87d8fc86 100644 --- a/openpype/plugins/publish/integrate_thumbnail.py +++ b/openpype/plugins/publish/integrate_thumbnail.py @@ -20,6 +20,7 @@ import pyblish.api from openpype.client import get_versions from openpype.client.operations import OperationsSession, new_thumbnail_doc +from openpype.pipeline.publish import get_publish_instance_label InstanceFilterResult = collections.namedtuple( "InstanceFilterResult", @@ -133,7 +134,7 @@ class IntegrateThumbnails(pyblish.api.ContextPlugin): filtered_instances = [] for instance in context: - instance_label = self._get_instance_label(instance) + instance_label = get_publish_instance_label(instance) # Skip instances without published representations # - there is no place where to put the thumbnail published_repres = instance.data.get("published_representations") @@ -248,7 +249,7 @@ class IntegrateThumbnails(pyblish.api.ContextPlugin): for instance_item in filtered_instance_items: instance, thumbnail_path, version_id = instance_item - instance_label = self._get_instance_label(instance) + instance_label = get_publish_instance_label(instance) version_doc = version_docs_by_str_id.get(version_id) if not version_doc: self.log.warning(( @@ -339,10 +340,3 @@ class IntegrateThumbnails(pyblish.api.ContextPlugin): )) op_session.commit() - - def _get_instance_label(self, instance): - return ( - instance.data.get("label") - or instance.data.get("name") - or "N/A" - ) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 8095d00103..89c2343ef7 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -40,6 +40,7 @@ from openpype.pipeline.create.context import ( CreatorsOperationFailed, ConvertorsOperationFailed, ) +from openpype.pipeline.publish import get_publish_instance_label # Define constant for plugin orders offset PLUGIN_ORDER_OFFSET = 0.5 @@ -346,7 +347,7 @@ class PublishReportMaker: def _extract_instance_data(self, instance, exists): return { "name": instance.data.get("name"), - "label": instance.data.get("label"), + "label": get_publish_instance_label(instance), "family": instance.data["family"], "families": instance.data.get("families") or [], "exists": exists, From 48b4934ee36628f57a895ff9e8cf7f349ddcea6d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 25 May 2023 13:33:11 +0200 Subject: [PATCH 137/189] limit number of ftrack events to query at once (#5033) --- openpype/modules/ftrack/ftrack_server/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/ftrack_server/lib.py b/openpype/modules/ftrack/ftrack_server/lib.py index eb64063fab..2226c85ef9 100644 --- a/openpype/modules/ftrack/ftrack_server/lib.py +++ b/openpype/modules/ftrack/ftrack_server/lib.py @@ -196,7 +196,7 @@ class ProcessEventHub(SocketBaseEventHub): {"pype_data.is_processed": False} ).sort( [("pype_data.stored", pymongo.ASCENDING)] - ) + ).limit(100) found = False for event_data in not_processed_events: From 318237ded65c42e04a61cc38ba91886c0becf7a4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 25 May 2023 16:38:01 +0200 Subject: [PATCH 138/189] breaking get_current_timeline into more functions --- openpype/hosts/resolve/api/__init__.py | 4 ++ openpype/hosts/resolve/api/lib.py | 83 ++++++++++++++++---------- openpype/hosts/resolve/api/plugin.py | 5 +- 3 files changed, 59 insertions(+), 33 deletions(-) diff --git a/openpype/hosts/resolve/api/__init__.py b/openpype/hosts/resolve/api/__init__.py index 00a598548e..2b4546f8d6 100644 --- a/openpype/hosts/resolve/api/__init__.py +++ b/openpype/hosts/resolve/api/__init__.py @@ -24,6 +24,8 @@ from .lib import ( get_project_manager, get_current_project, get_current_timeline, + get_any_timeline, + get_new_timeline, create_bin, get_media_pool_item, create_media_pool_item, @@ -95,6 +97,8 @@ __all__ = [ "get_project_manager", "get_current_project", "get_current_timeline", + "get_any_timeline", + "get_new_timeline", "create_bin", "get_media_pool_item", "create_media_pool_item", diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index d42521200a..a44c527f13 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -15,6 +15,7 @@ log = Logger.get_logger(__name__) self = sys.modules[__name__] self.project_manager = None self.media_storage = None +self.current_project = None # OpenPype sequential rename variables self.rename_index = 0 @@ -85,47 +86,60 @@ def get_media_storage(): def get_current_project(): - # initialize project manager - get_project_manager() + """Get current project object. + """ + if not self.current_project: + self.current_project = get_project_manager().GetCurrentProject() - return self.project_manager.GetCurrentProject() + return self.current_project -def get_current_timeline(new=False, get_any=False): +def get_current_timeline(new=False): """Get current timeline object. Args: - new (bool, optional): return only new timeline. Defaults to False. - get_any (bool, optional): return any even new if no timeline available. - Defaults to False. + new (bool)[optional]: [DEPRECATED] if True it will create + new timeline if none exists + + Returns: + TODO: will need to reflect future `None` + object: resolve.Timeline + """ + project = get_current_project() + timeline = project.GetCurrentTimeline() + + # return current timeline if any + if timeline: + return timeline + + # TODO: [deprecated] and will be removed in future + if new: + return get_new_timeline() + + +def get_any_timeline(): + """Get any timeline object. + + Returns: + object | None: resolve.Timeline + """ + project = get_current_project() + timeline_count = project.GetTimelineCount() + if timeline_count > 0: + return project.GetTimelineByIndex(1) + + +def get_new_timeline(): + """Get new timeline object. Returns: object: resolve.Timeline """ - # get current project project = get_current_project() - - timeline = project.GetCurrentTimeline() - - # return current timeline only if it is not new - if timeline and not new: - return timeline - - # if get_any is True then return any timeline - if get_any: - timeline_count = project.GetTimelineCount() - if timeline_count == 0: - # if there is no timeline then create a new one - new = True - else: - return project.GetTimelineByIndex(1) - - # create new timeline if new is True - if new: - media_pool = project.GetMediaPool() - new_timeline = media_pool.CreateEmptyTimeline(self.pype_timeline_name) - project.SetCurrentTimeline(new_timeline) - return new_timeline + media_pool = project.GetMediaPool() + new_timeline = media_pool.CreateEmptyTimeline(self.pype_timeline_name) + project.SetCurrentTimeline(new_timeline) + return new_timeline def create_bin(name: str, root: object = None) -> object: @@ -337,8 +351,13 @@ def get_current_timeline_items( track_type = track_type or "video" selecting_color = selecting_color or "Chocolate" project = get_current_project() - # make sure some timeline will be active with `any` argument - timeline = get_current_timeline(get_any=True) + + # get timeline anyhow + timeline = ( + get_current_timeline() or + get_any_timeline() or + get_new_timeline() + ) selected_clips = [] # get all tracks count filtered by track type diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index 609cff60f7..e5846c2fc2 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -327,7 +327,10 @@ class ClipLoader: self.active_timeline = options["timeline"] else: # create new sequence - self.active_timeline = lib.get_current_timeline(new=True) + self.active_timeline = ( + lib.get_current_timeline() or + lib.get_new_timeline() + ) else: self.active_timeline = lib.get_current_timeline() From c61dd1b24775c6438e3ba5844f5159ef1349b66a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 25 May 2023 17:18:23 +0200 Subject: [PATCH 139/189] utility scripts cosmetics only copy test and develop scripts if developer --- .../{__OpenPype__Menu__.py => OpenPype__Menu.py} | 0 openpype/hosts/resolve/utility_scripts/README.markdown | 1 - .../resolve/utility_scripts/{ => develop}/OTIO_export.py | 0 .../resolve/utility_scripts/{ => develop}/OTIO_import.py | 0 .../{ => develop}/OpenPype_sync_util_scripts.py | 0 openpype/hosts/resolve/utils.py | 9 ++++++++- 6 files changed, 8 insertions(+), 2 deletions(-) rename openpype/hosts/resolve/utility_scripts/{__OpenPype__Menu__.py => OpenPype__Menu.py} (100%) delete mode 100644 openpype/hosts/resolve/utility_scripts/README.markdown rename openpype/hosts/resolve/utility_scripts/{ => develop}/OTIO_export.py (100%) rename openpype/hosts/resolve/utility_scripts/{ => develop}/OTIO_import.py (100%) rename openpype/hosts/resolve/utility_scripts/{ => develop}/OpenPype_sync_util_scripts.py (100%) diff --git a/openpype/hosts/resolve/utility_scripts/__OpenPype__Menu__.py b/openpype/hosts/resolve/utility_scripts/OpenPype__Menu.py similarity index 100% rename from openpype/hosts/resolve/utility_scripts/__OpenPype__Menu__.py rename to openpype/hosts/resolve/utility_scripts/OpenPype__Menu.py diff --git a/openpype/hosts/resolve/utility_scripts/README.markdown b/openpype/hosts/resolve/utility_scripts/README.markdown deleted file mode 100644 index 8b13789179..0000000000 --- a/openpype/hosts/resolve/utility_scripts/README.markdown +++ /dev/null @@ -1 +0,0 @@ - diff --git a/openpype/hosts/resolve/utility_scripts/OTIO_export.py b/openpype/hosts/resolve/utility_scripts/develop/OTIO_export.py similarity index 100% rename from openpype/hosts/resolve/utility_scripts/OTIO_export.py rename to openpype/hosts/resolve/utility_scripts/develop/OTIO_export.py diff --git a/openpype/hosts/resolve/utility_scripts/OTIO_import.py b/openpype/hosts/resolve/utility_scripts/develop/OTIO_import.py similarity index 100% rename from openpype/hosts/resolve/utility_scripts/OTIO_import.py rename to openpype/hosts/resolve/utility_scripts/develop/OTIO_import.py diff --git a/openpype/hosts/resolve/utility_scripts/OpenPype_sync_util_scripts.py b/openpype/hosts/resolve/utility_scripts/develop/OpenPype_sync_util_scripts.py similarity index 100% rename from openpype/hosts/resolve/utility_scripts/OpenPype_sync_util_scripts.py rename to openpype/hosts/resolve/utility_scripts/develop/OpenPype_sync_util_scripts.py diff --git a/openpype/hosts/resolve/utils.py b/openpype/hosts/resolve/utils.py index 8e5dd9a188..9a161f4865 100644 --- a/openpype/hosts/resolve/utils.py +++ b/openpype/hosts/resolve/utils.py @@ -1,6 +1,6 @@ import os import shutil -from openpype.lib import Logger +from openpype.lib import Logger, is_running_from_build RESOLVE_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -41,6 +41,13 @@ def setup(env): # copy scripts into Resolve's utility scripts dir for directory, scripts in scripts.items(): for script in scripts: + if ( + is_running_from_build() and + script in ["tests", "develop"] + ): + # only copy those if started from build + continue + src = os.path.join(directory, script) dst = os.path.join(util_scripts_dir, script) log.info("Copying `{}` to `{}`...".format(src, dst)) From b47143b472eec6bb35ced0bbbb1d2b9a77c9acd4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 25 May 2023 17:41:36 +0200 Subject: [PATCH 140/189] collect frames to fix settings --- .../plugins/publish/collect_frames_fix.py | 21 ++++++++++++++----- .../defaults/project_settings/global.json | 8 ++++++- .../schemas/schema_global_publish.json | 20 ++++++++++++++++++ 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/openpype/plugins/publish/collect_frames_fix.py b/openpype/plugins/publish/collect_frames_fix.py index bdd49585a5..837738eb06 100644 --- a/openpype/plugins/publish/collect_frames_fix.py +++ b/openpype/plugins/publish/collect_frames_fix.py @@ -26,11 +26,13 @@ class CollectFramesFixDef( targets = ["local"] hosts = ["nuke"] families = ["render", "prerender"] - enabled = True + + rewrite_version_enable = False def process(self, instance): attribute_values = self.get_attr_values_from_data(instance.data) frames_to_fix = attribute_values.get("frames_to_fix") + rewrite_version = attribute_values.get("rewrite_version") if frames_to_fix: @@ -71,10 +73,19 @@ class CollectFramesFixDef( @classmethod def get_attribute_defs(cls): - return [ + attributes = [ TextDef("frames_to_fix", label="Frames to fix", placeholder="5,10-15", - regex="[0-9,-]+"), - BoolDef("rewrite_version", label="Rewrite latest version", - default=False), + regex="[0-9,-]+") ] + + if cls.rewrite_version_enable: + attributes.append( + BoolDef( + "rewrite_version", + label="Rewrite latest version", + default=False + ) + ) + + return attributes diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 75f335f1de..002e547feb 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -46,6 +46,10 @@ "enabled": false, "families": [] }, + "CollectFramesFixDef": { + "enabled": true, + "rewrite_version_enable": true + }, "ValidateEditorialAssetName": { "enabled": true, "optional": false @@ -252,7 +256,9 @@ } }, { - "families": ["review"], + "families": [ + "review" + ], "hosts": [ "maya", "houdini" diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index a7617918a3..8000e6156b 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -81,6 +81,26 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "checkbox_key": "enabled", + "key": "CollectFramesFixDef", + "label": "Collect Frames to Fix", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "rewrite_version_enable", + "label": "Rewrite latest version" + } + ] + }, { "type": "dict", "collapsible": true, From 31da5582fca78f9f84977700c69523e077df6e19 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 25 May 2023 21:39:07 +0200 Subject: [PATCH 141/189] make understandable label in settings --- .../schemas/projects_schema/schemas/schema_global_publish.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 8000e6156b..3164cfb62d 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -97,7 +97,7 @@ { "type": "boolean", "key": "rewrite_version_enable", - "label": "Rewrite latest version" + "label": "Show 'Rewrite latest version' toggle" } ] }, From 7e02416d30e4d33282a37b27331272701f276d17 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 26 May 2023 17:35:13 +0800 Subject: [PATCH 142/189] hound fix --- openpype/hosts/max/api/lib_renderproducts.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 1073a0e19e..19c1048496 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -150,7 +150,7 @@ class RenderProducts(object): def get_arnold_product_name(self, folder, startFrame, endFrame, fmt): """Get all the Arnold AOVs""" - aovs + aov_dict = {} amw = rt.MaxtoAOps.AOVsManagerWindow() aov_mgr = rt.renderers.current.AOVManager @@ -187,7 +187,7 @@ class RenderProducts(object): render_element = render_element.replace("\\", "/") render_dict.update({renderpass: render_element}) - return render_dirname + return render_dict def image_format(self): return self._project_settings["max"]["RenderSettings"]["image_format"] # noqa From 9f7f22961b6b30c256c7a60d8f16ea18058e1a62 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 26 May 2023 10:43:54 +0100 Subject: [PATCH 143/189] Improved implementation of UMap to use UAsset base code --- .../unreal/plugins/create/create_uasset.py | 24 ++- .../unreal/plugins/create/create_umap.py | 46 ------ .../hosts/unreal/plugins/load/load_uasset.py | 28 ++-- .../hosts/unreal/plugins/load/load_umap.py | 140 ------------------ .../unreal/plugins/publish/extract_uasset.py | 15 +- .../unreal/plugins/publish/extract_umap.py | 48 ------ 6 files changed, 49 insertions(+), 252 deletions(-) delete mode 100644 openpype/hosts/unreal/plugins/create/create_umap.py delete mode 100644 openpype/hosts/unreal/plugins/load/load_umap.py delete mode 100644 openpype/hosts/unreal/plugins/publish/extract_umap.py diff --git a/openpype/hosts/unreal/plugins/create/create_uasset.py b/openpype/hosts/unreal/plugins/create/create_uasset.py index c78518e86b..f70ecc55b3 100644 --- a/openpype/hosts/unreal/plugins/create/create_uasset.py +++ b/openpype/hosts/unreal/plugins/create/create_uasset.py @@ -17,6 +17,8 @@ class CreateUAsset(UnrealAssetCreator): family = "uasset" icon = "cube" + extension = ".uasset" + def create(self, subset_name, instance_data, pre_create_data): if pre_create_data.get("use_selection"): ar = unreal.AssetRegistryHelpers.get_asset_registry() @@ -37,10 +39,28 @@ class CreateUAsset(UnrealAssetCreator): f"{Path(obj).name} is not on the disk. Likely it needs to" "be saved first.") - if Path(sys_path).suffix != ".uasset": - raise CreatorError(f"{Path(sys_path).name} is not a UAsset.") + if Path(sys_path).suffix != self.extension: + raise CreatorError( + f"{Path(sys_path).name} is not a {self.label}.") super(CreateUAsset, self).create( subset_name, instance_data, pre_create_data) + + +class CreateUMap(CreateUAsset): + """Create Level.""" + + identifier = "io.ayon.creators.unreal.umap" + label = "Level" + family = "uasset" + extension = ".umap" + + def create(self, subset_name, instance_data, pre_create_data): + instance_data["families"] = ["umap"] + + super(CreateUMap, self).create( + subset_name, + instance_data, + pre_create_data) diff --git a/openpype/hosts/unreal/plugins/create/create_umap.py b/openpype/hosts/unreal/plugins/create/create_umap.py deleted file mode 100644 index 34aa8cdc00..0000000000 --- a/openpype/hosts/unreal/plugins/create/create_umap.py +++ /dev/null @@ -1,46 +0,0 @@ -# -*- coding: utf-8 -*- -from pathlib import Path - -import unreal - -from openpype.pipeline import CreatorError -from openpype.hosts.unreal.api.plugin import ( - UnrealAssetCreator, -) - - -class CreateUMap(UnrealAssetCreator): - """Create Level.""" - - identifier = "io.ayon.creators.unreal.umap" - label = "Level" - family = "uasset" - icon = "cube" - - def create(self, subset_name, instance_data, pre_create_data): - if pre_create_data.get("use_selection"): - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() - selection = [a.get_path_name() for a in sel_objects] - - if len(selection) != 1: - raise CreatorError("Please select only one object.") - - obj = selection[0] - - asset = ar.get_asset_by_object_path(obj).get_asset() - sys_path = unreal.SystemLibrary.get_system_path(asset) - - if not sys_path: - raise CreatorError( - f"{Path(obj).name} is not on the disk. Likely it needs to" - "be saved first.") - - if Path(sys_path).suffix != ".umap": - raise CreatorError(f"{Path(sys_path).name} is not a Level.") - - super(CreateUMap, self).create( - subset_name, - instance_data, - pre_create_data) diff --git a/openpype/hosts/unreal/plugins/load/load_uasset.py b/openpype/hosts/unreal/plugins/load/load_uasset.py index 7606bc14e4..44c87593e9 100644 --- a/openpype/hosts/unreal/plugins/load/load_uasset.py +++ b/openpype/hosts/unreal/plugins/load/load_uasset.py @@ -21,6 +21,8 @@ class UAssetLoader(plugin.Loader): icon = "cube" color = "orange" + extension = "uasset" + def load(self, context, name, namespace, options): """Load and containerise representation into Content Browser. @@ -42,11 +44,7 @@ class UAssetLoader(plugin.Loader): root = "/Game/Ayon/Assets" asset = context.get('asset').get('name') suffix = "_CON" - if asset: - asset_name = "{}_{}".format(asset, name) - else: - asset_name = "{}".format(name) - + asset_name = f"{asset}_{name}" if asset else f"{name}" tools = unreal.AssetToolsHelpers().get_asset_tools() asset_dir, container_name = tools.create_unique_asset_name( f"{root}/{asset}/{name}", suffix="" @@ -61,7 +59,7 @@ class UAssetLoader(plugin.Loader): Path(unreal.Paths.project_content_dir()).as_posix(), 1) - shutil.copy(self.fname, f"{destination_path}/{name}.uasset") + shutil.copy(self.fname, f"{destination_path}/{name}.{self.extension}") # Create Asset Container unreal_pipeline.create_container( @@ -107,15 +105,15 @@ class UAssetLoader(plugin.Loader): for asset in asset_content: obj = ar.get_asset_by_object_path(asset).get_asset() - if not obj.get_class().get_name() == 'AyonAssetContainer': + if obj.get_class().get_name() != 'AyonAssetContainer': unreal.EditorAssetLibrary.delete_asset(asset) update_filepath = get_representation_path(representation) - shutil.copy(update_filepath, f"{destination_path}/{name}.uasset") + shutil.copy( + update_filepath, f"{destination_path}/{name}.{self.extension}") - container_path = "{}/{}".format(container["namespace"], - container["objectName"]) + container_path = f'{container["namespace"]}/{container["objectName"]}' # update metadata unreal_pipeline.imprint( container_path, @@ -143,3 +141,13 @@ class UAssetLoader(plugin.Loader): if len(asset_content) == 0: unreal.EditorAssetLibrary.delete_directory(parent_path) + + +class UMapLoader(UAssetLoader): + """Load Level.""" + + families = ["uasset"] + label = "Load Level" + representations = ["umap"] + + extension = "umap" diff --git a/openpype/hosts/unreal/plugins/load/load_umap.py b/openpype/hosts/unreal/plugins/load/load_umap.py deleted file mode 100644 index f467fe6b3b..0000000000 --- a/openpype/hosts/unreal/plugins/load/load_umap.py +++ /dev/null @@ -1,140 +0,0 @@ -# -*- coding: utf-8 -*- -"""Load Level.""" -from pathlib import Path -import shutil - -from openpype.pipeline import ( - get_representation_path, - AYON_CONTAINER_ID -) -from openpype.hosts.unreal.api import plugin -from openpype.hosts.unreal.api import pipeline as unreal_pipeline -import unreal # noqa - - -class UMapLoader(plugin.Loader): - """Load Level.""" - - families = ["uasset"] - label = "Load Level" - representations = ["umap"] - icon = "cube" - color = "orange" - - def load(self, context, name, namespace, options): - """Load and containerise representation into Content Browser. - - Args: - context (dict): application context - name (str): subset name - namespace (str): in Unreal this is basically path to container. - This is not passed here, so namespace is set - by `containerise()` because only then we know - real path. - options (dict): Those would be data to be imprinted. This is not - used now, data are imprinted by `containerise()`. - - Returns: - list(str): list of container content - """ - - # Create directory for asset and Ayon container - root = "/Game/Ayon/Assets" - asset = context.get('asset').get('name') - suffix = "_CON" - asset_name = f"{asset}_{name}" if asset else f"{name}" - tools = unreal.AssetToolsHelpers().get_asset_tools() - asset_dir, container_name = tools.create_unique_asset_name( - f"{root}/{asset}/{name}", suffix="" - ) - - container_name += suffix - - unreal.EditorAssetLibrary.make_directory(asset_dir) - - destination_path = asset_dir.replace( - "/Game", - Path(unreal.Paths.project_content_dir()).as_posix(), - 1) - - shutil.copy(self.fname, f"{destination_path}/{name}.uasset") - - # Create Asset Container - unreal_pipeline.create_container( - container=container_name, path=asset_dir) - - data = { - "schema": "ayon:container-2.0", - "id": AYON_CONTAINER_ID, - "asset": asset, - "namespace": asset_dir, - "container_name": container_name, - "asset_name": asset_name, - "loader": str(self.__class__.__name__), - "representation": context["representation"]["_id"], - "parent": context["representation"]["parent"], - "family": context["representation"]["context"]["family"] - } - unreal_pipeline.imprint(f"{asset_dir}/{container_name}", data) - - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=True - ) - - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) - - return asset_content - - def update(self, container, representation): - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - asset_dir = container["namespace"] - name = representation["context"]["subset"] - - destination_path = asset_dir.replace( - "/Game", - Path(unreal.Paths.project_content_dir()).as_posix(), - 1) - - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=False, include_folder=True - ) - - for asset in asset_content: - obj = ar.get_asset_by_object_path(asset).get_asset() - if obj.get_class().get_name() != 'AyonAssetContainer': - unreal.EditorAssetLibrary.delete_asset(asset) - - update_filepath = get_representation_path(representation) - - shutil.copy(update_filepath, f"{destination_path}/{name}.umap") - - container_path = f'{container["namespace"]}/{container["objectName"]}' - # update metadata - unreal_pipeline.imprint( - container_path, - { - "representation": str(representation["_id"]), - "parent": str(representation["parent"]) - }) - - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=True - ) - - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) - - def remove(self, container): - path = container["namespace"] - parent_path = Path(path).parent.as_posix() - - unreal.EditorAssetLibrary.delete_directory(path) - - asset_content = unreal.EditorAssetLibrary.list_assets( - parent_path, recursive=False - ) - - if len(asset_content) == 0: - unreal.EditorAssetLibrary.delete_directory(parent_path) diff --git a/openpype/hosts/unreal/plugins/publish/extract_uasset.py b/openpype/hosts/unreal/plugins/publish/extract_uasset.py index f719df2a82..48b62faa97 100644 --- a/openpype/hosts/unreal/plugins/publish/extract_uasset.py +++ b/openpype/hosts/unreal/plugins/publish/extract_uasset.py @@ -11,16 +11,17 @@ class ExtractUAsset(publish.Extractor): label = "Extract UAsset" hosts = ["unreal"] - families = ["uasset"] + families = ["uasset", "umap"] optional = True def process(self, instance): + extension = ( + "umap" if "umap" in instance.data.get("families") else "uasset") ar = unreal.AssetRegistryHelpers.get_asset_registry() self.log.info("Performing extraction..") - staging_dir = self.staging_dir(instance) - filename = "{}.uasset".format(instance.name) + filename = f"{instance.name}.{extension}" members = instance.data.get("members", []) @@ -36,13 +37,15 @@ class ExtractUAsset(publish.Extractor): shutil.copy(sys_path, staging_dir) + self.log.info(f"instance.data: {instance.data}") + if "representations" not in instance.data: instance.data["representations"] = [] representation = { - 'name': 'uasset', - 'ext': 'uasset', - 'files': filename, + "name": extension, + "ext": extension, + "files": filename, "stagingDir": staging_dir, } instance.data["representations"].append(representation) diff --git a/openpype/hosts/unreal/plugins/publish/extract_umap.py b/openpype/hosts/unreal/plugins/publish/extract_umap.py deleted file mode 100644 index 3812834430..0000000000 --- a/openpype/hosts/unreal/plugins/publish/extract_umap.py +++ /dev/null @@ -1,48 +0,0 @@ -from pathlib import Path -import shutil - -import unreal - -from openpype.pipeline import publish - - -class ExtractUMap(publish.Extractor): - """Extract a UMap.""" - - label = "Extract Level" - hosts = ["unreal"] - families = ["uasset"] - optional = True - - def process(self, instance): - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - self.log.info("Performing extraction..") - - staging_dir = self.staging_dir(instance) - filename = f"{instance.name}.umap" - - members = instance.data.get("members", []) - - if not members: - raise RuntimeError("No members found in instance.") - - # UAsset publishing supports only one member - obj = members[0] - - asset = ar.get_asset_by_object_path(obj).get_asset() - sys_path = unreal.SystemLibrary.get_system_path(asset) - filename = Path(sys_path).name - - shutil.copy(sys_path, staging_dir) - - if "representations" not in instance.data: - instance.data["representations"] = [] - - representation = { - 'name': 'umap', - 'ext': 'umap', - 'files': filename, - "stagingDir": staging_dir, - } - instance.data["representations"].append(representation) From 5f98c278361d9354565261174e51b43b5acaffa9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 26 May 2023 12:00:09 +0200 Subject: [PATCH 144/189] apply settings on publish plugins can expect only project settings (#5037) --- openpype/pipeline/publish/lib.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index e87b865dce..f228709b3b 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -12,7 +12,8 @@ import pyblish.api from openpype.lib import ( Logger, import_filepath, - filter_profiles + filter_profiles, + is_func_signature_supported, ) from openpype.settings import ( get_project_settings, @@ -498,12 +499,26 @@ def filter_pyblish_plugins(plugins): # iterate over plugins for plugin in plugins[:]: # Apply settings to plugins - if hasattr(plugin, "apply_settings"): + + apply_settings_func = getattr(plugin, "apply_settings", None) + if apply_settings_func is not None: # Use classmethod 'apply_settings' # - can be used to target settings from custom settings place # - skip default behavior when successful try: - plugin.apply_settings(project_settings, system_settings) + # Support to pass only project settings + # - make sure that both settings are passed, when can be + # - that covers cases when *args are in method parameters + both_supported = is_func_signature_supported( + apply_settings_func, project_settings, system_settings + ) + project_supported = is_func_signature_supported( + apply_settings_func, project_settings + ) + if not both_supported and project_supported: + plugin.apply_settings(project_settings) + else: + plugin.apply_settings(project_settings, system_settings) except Exception: log.warning( From e633cc7decbcfb8642f7d66ad17fe4219805e834 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 26 May 2023 18:19:50 +0800 Subject: [PATCH 145/189] expected file can get the aov path --- openpype/hosts/max/api/lib_renderproducts.py | 4 +++- .../max/plugins/publish/collect_render.py | 19 ++----------------- .../plugins/publish/submit_publish_job.py | 2 +- 3 files changed, 6 insertions(+), 19 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 19c1048496..ba1ffc3a5e 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -70,7 +70,7 @@ class RenderProducts(object): return rgba_render_list, render_elem_list - def get_aov(self): + def get_aovs(self): folder = rt.maxFilePath folder = folder.replace("\\", "/") setting = self._project_settings @@ -177,6 +177,8 @@ class RenderProducts(object): render_elem = rt.maxOps.GetCurRenderElementMgr() render_elem_num = render_elem.NumRenderElements() + if render_elem_num < 1: + return # get render elements from the renders for i in range(render_elem_num): renderlayer_name = render_elem.GetRenderElement(i) diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 652c2e1d2c..c4a44a5b11 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -45,23 +45,8 @@ class CollectRender(pyblish.api.InstancePlugin): } folder = folder.replace("\\", "/") - if aov_list: - if renderer in [ - "ART_Renderer", - "V_Ray_6_Hotfix_3", - "V_Ray_GPU_6_Hotfix_3" - "Redshift_Renderer", - "Default_Scanline_Renderer", - "Quicksilver_Hardware_Renderer", - ]: - - render_element = RenderProducts().get_aov() - files_by_aov.update(render_element) - self.log.debug(files_by_aov) - - if renderer == "Arnold": - aovs = RenderProducts().get_aovs() - files_by_aov.update(aovs) + aovs = RenderProducts().get_aovs() + files_by_aov.update(aovs) if "expectedFiles" not in instance.data: instance.data["expectedFiles"] = list() diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 7133cff058..68eb0a437d 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -348,7 +348,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): self.log.info("Submitting Deadline job ...") url = "{}/api/jobs".format(self.deadline_url) - response = requests.post(url, json=payload, timeout=10, verify=False) + response = requests.post(url, json=payload, timeout=10) if not response.ok: raise Exception(response.text) From 905c3dbd249bce6c7d229e10466b3035aa2a6d40 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 26 May 2023 12:01:36 +0100 Subject: [PATCH 146/189] Fix problem when trying to load the same level multiple times --- .../hosts/unreal/plugins/load/load_uasset.py | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_uasset.py b/openpype/hosts/unreal/plugins/load/load_uasset.py index 44c87593e9..30f63abe39 100644 --- a/openpype/hosts/unreal/plugins/load/load_uasset.py +++ b/openpype/hosts/unreal/plugins/load/load_uasset.py @@ -50,16 +50,23 @@ class UAssetLoader(plugin.Loader): f"{root}/{asset}/{name}", suffix="" ) - container_name += suffix + unique_number = 1 + while unreal.EditorAssetLibrary.does_directory_exist( + f"{asset_dir}_{unique_number:02}" + ): + unique_number += 1 + + asset_dir = f"{asset_dir}_{unique_number:02}" + container_name = f"{container_name}_{unique_number:02}{suffix}" unreal.EditorAssetLibrary.make_directory(asset_dir) destination_path = asset_dir.replace( - "/Game", - Path(unreal.Paths.project_content_dir()).as_posix(), - 1) + "/Game", Path(unreal.Paths.project_content_dir()).as_posix(), 1) - shutil.copy(self.fname, f"{destination_path}/{name}.{self.extension}") + shutil.copy( + self.fname, + f"{destination_path}/{name}_{unique_number:02}.{self.extension}") # Create Asset Container unreal_pipeline.create_container( @@ -75,7 +82,7 @@ class UAssetLoader(plugin.Loader): "loader": str(self.__class__.__name__), "representation": context["representation"]["_id"], "parent": context["representation"]["parent"], - "family": context["representation"]["context"]["family"] + "family": context["representation"]["context"]["family"], } unreal_pipeline.imprint(f"{asset_dir}/{container_name}", data) @@ -94,10 +101,10 @@ class UAssetLoader(plugin.Loader): asset_dir = container["namespace"] name = representation["context"]["subset"] + unique_number = container["container_name"].split("_")[-2] + destination_path = asset_dir.replace( - "/Game", - Path(unreal.Paths.project_content_dir()).as_posix(), - 1) + "/Game", Path(unreal.Paths.project_content_dir()).as_posix(), 1) asset_content = unreal.EditorAssetLibrary.list_assets( asset_dir, recursive=False, include_folder=True @@ -105,13 +112,14 @@ class UAssetLoader(plugin.Loader): for asset in asset_content: obj = ar.get_asset_by_object_path(asset).get_asset() - if obj.get_class().get_name() != 'AyonAssetContainer': + if obj.get_class().get_name() != "AyonAssetContainer": unreal.EditorAssetLibrary.delete_asset(asset) update_filepath = get_representation_path(representation) shutil.copy( - update_filepath, f"{destination_path}/{name}.{self.extension}") + update_filepath, + f"{destination_path}/{name}_{unique_number}.{self.extension}") container_path = f'{container["namespace"]}/{container["objectName"]}' # update metadata @@ -119,8 +127,9 @@ class UAssetLoader(plugin.Loader): container_path, { "representation": str(representation["_id"]), - "parent": str(representation["parent"]) - }) + "parent": str(representation["parent"]), + } + ) asset_content = unreal.EditorAssetLibrary.list_assets( asset_dir, recursive=True, include_folder=True From 2d3ba2af0576d5a201fafa0a0957e18d0aa9d00a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 26 May 2023 19:36:14 +0800 Subject: [PATCH 147/189] add _beauty to subset name --- openpype/hosts/max/api/lib_renderproducts.py | 82 +++++++++++++------ .../max/plugins/publish/collect_render.py | 8 +- .../plugins/publish/submit_publish_job.py | 2 +- 3 files changed, 62 insertions(+), 30 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index ba1ffc3a5e..a93a1d821d 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -70,16 +70,26 @@ class RenderProducts(object): return rgba_render_list, render_elem_list - def get_aovs(self): + def get_aovs(self, container): folder = rt.maxFilePath + file = rt.maxFileName folder = folder.replace("\\", "/") setting = self._project_settings + render_folder = get_default_render_folder(setting) + filename, ext = os.path.splitext(file) + + output_file = os.path.join(folder, + render_folder, + filename, + container) + setting = self._project_settings img_fmt = setting["max"]["RenderSettings"]["image_format"] # noqa startFrame = int(rt.rendStart) endFrame = int(rt.rendEnd) + 1 renderer_class = get_current_renderer() renderer = str(renderer_class).split(":")[0] + render_dict = {} if renderer in [ "ART_Renderer", "Redshift_Renderer", @@ -88,12 +98,22 @@ class RenderProducts(object): "Default_Scanline_Renderer", "Quicksilver_Hardware_Renderer", ]: - render_dict = self.get_render_elements_name( - folder, startFrame, endFrame, img_fmt) + render_name = self.get_render_elements_name() + if render_name: + for name in render_name: + render_dict.update({ + name: self.get_expected_render_elements( + output_file, name, startFrame, endFrame, img_fmt) + }) if renderer == "Arnold": - render_dict = self.get_arnold_product_name( - folder, startFrame, endFrame, img_fmt) + render_name = self.get_arnold_product_name() + if render_name: + for name in render_name: + render_dict.update({ + name: self.get_expected_arnold_product( + output_file, name, startFrame, endFrame, img_fmt) + }) return render_dict @@ -148,9 +168,9 @@ class RenderProducts(object): return render_dirname - def get_arnold_product_name(self, folder, startFrame, endFrame, fmt): - """Get all the Arnold AOVs""" - aov_dict = {} + def get_arnold_product_name(self): + """Get all the Arnold AOVs name""" + aov_name = [] amw = rt.MaxtoAOps.AOVsManagerWindow() aov_mgr = rt.renderers.current.AOVManager @@ -161,20 +181,27 @@ class RenderProducts(object): for i in range(aov_group_num): # get the specific AOV group for aov in aov_mgr.drivers[i].aov_list: - for f in range(startFrame, endFrame): - render_element = f"{folder}_{aov.name}.{f}.{fmt}" - render_element = render_element.replace("\\", "/") - aov = str(aov.name) - aov_dict.update({aov: render_element}) + aov_name.append(aov.name) + # close the AOVs manager window amw.close() - return aov_dict + return aov_name - def get_render_elements_name(self, folder, startFrame, endFrame, fmt): - """Get all the render element output files. """ - render_dict = {} + def get_expected_arnold_product(self, folder, name, + startFrame, endFrame, fmt): + """Get all the expected Arnold AOVs""" + aov_list = [] + for f in range(startFrame, endFrame): + render_element = f"{folder}_{name}.{f}.{fmt}" + render_element = render_element.replace("\\", "/") + aov_list.append(render_element) + return aov_list + + def get_render_elements_name(self): + """Get all the render element names. """ + render_name = [] render_elem = rt.maxOps.GetCurRenderElementMgr() render_elem_num = render_elem.NumRenderElements() if render_elem_num < 1: @@ -182,14 +209,21 @@ class RenderProducts(object): # get render elements from the renders for i in range(render_elem_num): renderlayer_name = render_elem.GetRenderElement(i) - target, renderpass = str(renderlayer_name).split(":") - if renderlayer_name.enabled: - for f in range(startFrame, endFrame): - render_element = f"{folder}_{renderpass}.{f}.{fmt}" - render_element = render_element.replace("\\", "/") - render_dict.update({renderpass: render_element}) + if renderlayer_name.enabled or "Cryptomatte" in renderlayer_name: + target, renderpass = str(renderlayer_name).split(":") + render_name.append(renderpass) + return render_name - return render_dict + def get_expected_render_elements(self, folder, name, + startFrame, endFrame, fmt): + """Get all the expected render element output files. """ + render_elements = [] + for f in range(startFrame, endFrame): + render_element = f"{folder}_{name}.{f}.{fmt}" + render_element = render_element.replace("\\", "/") + render_elements.append(render_element) + + return render_elements def image_format(self): return self._project_settings["max"]["RenderSettings"]["image_format"] # noqa diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index c4a44a5b11..1282c9b3fe 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -41,11 +41,11 @@ class CollectRender(pyblish.api.InstancePlugin): full_render_list = beauty_list files_by_aov = { - "_": beauty_list + "beauty": beauty_list } folder = folder.replace("\\", "/") - aovs = RenderProducts().get_aovs() + aovs = RenderProducts().get_aovs(instance.name) files_by_aov.update(aovs) if "expectedFiles" not in instance.data: @@ -78,8 +78,8 @@ class CollectRender(pyblish.api.InstancePlugin): instance.data["attachTo"] = [] data = { - "subset": instance.name, "asset": asset, + "subset": str(instance.name), "publish": True, "maxversion": str(get_max_version()), "imageFormat": img_format, @@ -95,5 +95,3 @@ class CollectRender(pyblish.api.InstancePlugin): } instance.data.update(data) self.log.info("data: {0}".format(data)) - files = instance.data["expectedFiles"] - self.log.debug("expectedFiles: {0}".format(files)) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 68eb0a437d..7133cff058 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -348,7 +348,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): self.log.info("Submitting Deadline job ...") url = "{}/api/jobs".format(self.deadline_url) - response = requests.post(url, json=payload, timeout=10) + response = requests.post(url, json=payload, timeout=10, verify=False) if not response.ok: raise Exception(response.text) From 4303b281aab9c157d109e034c68d5d8816fd4450 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 26 May 2023 19:38:05 +0800 Subject: [PATCH 148/189] hound fix --- openpype/hosts/max/api/lib_renderproducts.py | 4 ++-- openpype/hosts/max/plugins/publish/collect_render.py | 4 +--- .../modules/deadline/plugins/publish/submit_publish_job.py | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index a93a1d821d..b33d0c5751 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -103,7 +103,7 @@ class RenderProducts(object): for name in render_name: render_dict.update({ name: self.get_expected_render_elements( - output_file, name, startFrame, endFrame, img_fmt) + output_file, name, startFrame, endFrame, img_fmt) }) if renderer == "Arnold": @@ -112,7 +112,7 @@ class RenderProducts(object): for name in render_name: render_dict.update({ name: self.get_expected_arnold_product( - output_file, name, startFrame, endFrame, img_fmt) + output_file, name, startFrame, endFrame, img_fmt) }) return render_dict diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 1282c9b3fe..a21ccf532e 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -6,7 +6,7 @@ import pyblish.api from pymxs import runtime as rt from openpype.pipeline import get_current_asset_name from openpype.hosts.max.api import colorspace -from openpype.hosts.max.api.lib import get_max_version, get_current_renderer +from openpype.hosts.max.api.lib import get_max_version from openpype.hosts.max.api.lib_renderproducts import RenderProducts from openpype.client import get_last_version_by_subset_name @@ -29,8 +29,6 @@ class CollectRender(pyblish.api.InstancePlugin): context.data['currentFile'] = current_file asset = get_current_asset_name() - renderer_class = get_current_renderer() - renderer = str(renderer_class).split(":")[0] beauty_list, aov_list = RenderProducts().render_product(instance.name) full_render_list = list() if aov_list: diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 7133cff058..68eb0a437d 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -348,7 +348,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): self.log.info("Submitting Deadline job ...") url = "{}/api/jobs".format(self.deadline_url) - response = requests.post(url, json=payload, timeout=10, verify=False) + response = requests.post(url, json=payload, timeout=10) if not response.ok: raise Exception(response.text) From 6843ae85321ae617dfea564086c5d58fa34f7daf Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 26 May 2023 14:44:47 +0200 Subject: [PATCH 149/189] General: Small code cleanups (#5034) * make sure the message type is set and unset correctly * Update dummy data in readme * remove debug message from main thread callbacks * removed unused import * cleanup code in muster addon * simplified 'get_publish_instance_label' function * even better json file handling Co-authored-by: Roy Nieterau --------- Co-authored-by: Roy Nieterau --- openpype/modules/ftrack/tray/login_dialog.py | 2 -- openpype/modules/muster/muster.py | 14 +++++-------- openpype/pipeline/publish/lib.py | 21 ++++++-------------- openpype/tools/utils/lib.py | 1 - openpype/tools/utils/overlay_messages.py | 3 +-- tests/README.md | 18 ++++++++--------- 6 files changed, 20 insertions(+), 39 deletions(-) diff --git a/openpype/modules/ftrack/tray/login_dialog.py b/openpype/modules/ftrack/tray/login_dialog.py index f374a71178..a8abdaf191 100644 --- a/openpype/modules/ftrack/tray/login_dialog.py +++ b/openpype/modules/ftrack/tray/login_dialog.py @@ -1,5 +1,3 @@ -import os - import requests from qtpy import QtCore, QtGui, QtWidgets diff --git a/openpype/modules/muster/muster.py b/openpype/modules/muster/muster.py index 77b9214a5a..0cdb1230c8 100644 --- a/openpype/modules/muster/muster.py +++ b/openpype/modules/muster/muster.py @@ -1,7 +1,9 @@ import os import json + import appdirs import requests + from openpype.modules import OpenPypeModule, ITrayModule @@ -110,16 +112,10 @@ class MusterModule(OpenPypeModule, ITrayModule): self.save_credentials(token) def save_credentials(self, token): - """ - Save credentials to JSON file - """ - data = { - 'token': token - } + """Save credentials to JSON file.""" - file = open(self.cred_path, 'w') - file.write(json.dumps(data)) - file.close() + with open(self.cred_path, "w") as f: + json.dump({'token': token}, f) def show_login(self): """ diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index f228709b3b..471be5ddb8 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -31,8 +31,6 @@ from .contants import ( TRANSIENT_DIR_TEMPLATE ) -_ARG_PLACEHOLDER = object() - def get_template_name_profiles( project_name, project_settings=None, logger=None @@ -885,31 +883,24 @@ def add_repre_files_for_cleanup(instance, repre): instance.context.data["cleanupFullPaths"].append(expected_file) -def get_publish_instance_label(instance, default=_ARG_PLACEHOLDER): +def get_publish_instance_label(instance): """Try to get label from pyblish instance. - First are checked 'label' and 'name' keys in instance data. If are not set - a default value is returned. Instance object is converted to string - if default value is not specific. + First are used values in instance data under 'label' and 'name' keys. Then + is used string conversion of instance object -> 'instance._name'. Todos: Maybe 'subset' key could be used too. Args: instance (pyblish.api.Instance): Pyblish instance. - default (Optional[Any]): Default value to return if any Returns: - Union[Any]: Instance label or default label. + str: Instance label. """ - label = ( + return ( instance.data.get("label") or instance.data.get("name") + or str(instance) ) - if label: - return label - - if default is _ARG_PLACEHOLDER: - return str(instance) - return default diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index 950c782727..58ece7c68f 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -872,7 +872,6 @@ class WrappedCallbackItem: self.log.warning("- item is already processed") return - self.log.debug("Running callback: {}".format(str(self._callback))) try: result = self._callback(*self._args, **self._kwargs) self._result = result diff --git a/openpype/tools/utils/overlay_messages.py b/openpype/tools/utils/overlay_messages.py index 180d7eae97..4da266bcf7 100644 --- a/openpype/tools/utils/overlay_messages.py +++ b/openpype/tools/utils/overlay_messages.py @@ -127,8 +127,7 @@ class OverlayMessageWidget(QtWidgets.QFrame): if timeout: self._timeout_timer.setInterval(timeout) - if message_type: - set_style_property(self, "type", message_type) + set_style_property(self, "type", message_type) self._timeout_timer.start() diff --git a/tests/README.md b/tests/README.md index d36b6534f8..20847b2449 100644 --- a/tests/README.md +++ b/tests/README.md @@ -15,16 +15,16 @@ Structure: - openpype/modules/MODULE_NAME - structure follow directory structure in code base - fixture - sample data `(MongoDB dumps, test files etc.)` - `tests.py` - single or more pytest files for MODULE_NAME -- unit - quick unit test - - MODULE_NAME +- unit - quick unit test + - MODULE_NAME - fixture - `tests.py` - + How to run: ---------- - use Openpype command 'runtests' from command line (`.venv` in ${OPENPYPE_ROOT} must be activated to use configured Python!) -- `python ${OPENPYPE_ROOT}/start.py runtests` - + By default, this command will run all tests in ${OPENPYPE_ROOT}/tests. Specific location could be provided to this command as an argument, either as absolute path, or relative path to ${OPENPYPE_ROOT}. @@ -41,17 +41,15 @@ In some cases your tests might be so localized, that you don't care about all en In that case you might add this dummy configuration BEFORE any imports in your test file ``` import os -os.environ["AVALON_MONGO"] = "mongodb://localhost:27017" +os.environ["OPENPYPE_DEBUG"] = "1" os.environ["OPENPYPE_MONGO"] = "mongodb://localhost:27017" -os.environ["AVALON_DB"] = "avalon" os.environ["OPENPYPE_DATABASE_NAME"] = "openpype" -os.environ["AVALON_TIMEOUT"] = '3000' -os.environ["OPENPYPE_DEBUG"] = "3" -os.environ["AVALON_CONFIG"] = "pype" +os.environ["AVALON_DB"] = "avalon" +os.environ["AVALON_TIMEOUT"] = "3000" os.environ["AVALON_ASSET"] = "Asset" os.environ["AVALON_PROJECT"] = "test_project" ``` (AVALON_ASSET and AVALON_PROJECT values should exist in your environment) This might be enough to run your test file separately. Do not commit this skeleton though. -Use only when you know what you are doing! \ No newline at end of file +Use only when you know what you are doing! From c14525f371aa8b8b2a524022f860cede764f7d0d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 26 May 2023 23:13:17 +0800 Subject: [PATCH 150/189] fix the wrong directory for rendering --- .../max/plugins/publish/collect_render.py | 2 +- .../deadline/abstract_submit_deadline.py | 2 +- .../plugins/publish/submit_max_deadline.py | 40 +++++++++++-------- .../plugins/publish/submit_publish_job.py | 6 ++- 4 files changed, 30 insertions(+), 20 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index a21ccf532e..c8e407bbe4 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -39,7 +39,7 @@ class CollectRender(pyblish.api.InstancePlugin): full_render_list = beauty_list files_by_aov = { - "beauty": beauty_list + "max_beauty": beauty_list } folder = folder.replace("\\", "/") diff --git a/openpype/modules/deadline/abstract_submit_deadline.py b/openpype/modules/deadline/abstract_submit_deadline.py index 558a637e4b..6694f638d6 100644 --- a/openpype/modules/deadline/abstract_submit_deadline.py +++ b/openpype/modules/deadline/abstract_submit_deadline.py @@ -582,7 +582,7 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin): metadata_folder = metadata_folder.replace(orig_scene, new_scene) instance.data["publishRenderMetadataFolder"] = metadata_folder - + self.log.debug(f"MetadataFolder:{metadata_folder}") self.log.info("Scene name was switched {} -> {}".format( orig_scene, new_scene )) diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index c678c0fb6e..d2de9160fb 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -132,8 +132,8 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, # Add list of expected files to job # --------------------------------- - files = instance.data.get("files") - for filepath in files: + exp = instance.data.get("expectedFiles") + for filepath in self._iter_expected_files(exp): job_info.OutputDirectory += os.path.dirname(filepath) job_info.OutputFilename += os.path.basename(filepath) @@ -162,10 +162,11 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, instance = self._instance filepath = self.scene_path - files = instance.data["files"] + files = instance.data["expectedFiles"] if not files: raise RuntimeError("No Render Elements found!") - output_dir = os.path.dirname(files[0]) + first_file = next(self._iter_expected_files(files)) + output_dir = os.path.dirname(first_file) instance.data["outputDir"] = output_dir instance.data["toBeRenderedOn"] = "deadline" @@ -202,17 +203,11 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, old_output_dir = os.path.dirname(files[0]) output_beauty = RenderSettings().get_render_output(instance.name, old_output_dir) - filepath = self.scene_path - - def _clean_name(path): - return os.path.splitext(os.path.basename(path))[0] - - new_scene = _clean_name(filepath) - orig_scene = _clean_name(instance.context.data["currentFile"]) - - output_beauty = output_beauty.replace(orig_scene, new_scene) - output_beauty = output_beauty.replace("\\", "/") - plugin_data["RenderOutput"] = output_beauty + files = instance.data["expectedFiles"] + first_file = next(self._iter_expected_files(files)) + rgb_bname = os.path.basename(output_beauty) + dir = os.path.dirname(first_file) + plugin_data["RenderOutput"] = f"{dir}/{rgb_bname}" renderer_class = get_current_renderer() renderer = str(renderer_class).split(":")[0] @@ -226,14 +221,25 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, ]: render_elem_list = RenderSettings().get_render_element() for i, element in enumerate(render_elem_list): - element = element.replace(orig_scene, new_scene) - plugin_data["RenderElementOutputFilename%d" % i] = element # noqa + elem_bname = os.path.basename(element) + new_elem = f"{dir}/{elem_bname}" + plugin_data["RenderElementOutputFilename%d" % i] = new_elem # noqa self.log.debug("plugin data:{}".format(plugin_data)) plugin_info.update(plugin_data) return job_info, plugin_info + @staticmethod + def _iter_expected_files(exp): + if isinstance(exp[0], dict): + for _aov, files in exp[0].items(): + for file in files: + yield file + else: + for file in exp: + yield file + @classmethod def get_attribute_defs(cls): defs = super(MaxSubmitDeadline, cls).get_attribute_defs() diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 68eb0a437d..fb0608908f 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -348,7 +348,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): self.log.info("Submitting Deadline job ...") url = "{}/api/jobs".format(self.deadline_url) - response = requests.post(url, json=payload, timeout=10) + response = requests.post(url, json=payload, timeout=10, verify=False) if not response.ok: raise Exception(response.text) @@ -488,11 +488,15 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): if cam: if aov: subset_name = '{}_{}_{}'.format(group_name, cam, aov) + if aov == "max_beauty": + subset_name = '{}_{}'.format(group_name, cam) else: subset_name = '{}_{}'.format(group_name, cam) else: if aov: subset_name = '{}_{}'.format(group_name, aov) + if aov == "max_beauty": + subset_name = '{}'.format(group_name) else: subset_name = '{}'.format(group_name) From 32562e0b39500996bc25ad2173d401b83d60a48f Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Sat, 27 May 2023 00:53:40 +0800 Subject: [PATCH 151/189] give beauty name to RGB --- openpype/hosts/max/plugins/publish/collect_render.py | 2 +- .../modules/deadline/plugins/publish/submit_publish_job.py | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index c8e407bbe4..a21ccf532e 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -39,7 +39,7 @@ class CollectRender(pyblish.api.InstancePlugin): full_render_list = beauty_list files_by_aov = { - "max_beauty": beauty_list + "beauty": beauty_list } folder = folder.replace("\\", "/") diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index fb0608908f..7133cff058 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -488,15 +488,11 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): if cam: if aov: subset_name = '{}_{}_{}'.format(group_name, cam, aov) - if aov == "max_beauty": - subset_name = '{}_{}'.format(group_name, cam) else: subset_name = '{}_{}'.format(group_name, cam) else: if aov: subset_name = '{}_{}'.format(group_name, aov) - if aov == "max_beauty": - subset_name = '{}'.format(group_name) else: subset_name = '{}'.format(group_name) From 271d017bdb368b6cbfdb95087df6da957da43f4a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Sat, 27 May 2023 00:56:11 +0800 Subject: [PATCH 152/189] remove the print function, and set verify to true for payload in publishing job --- openpype/modules/deadline/abstract_submit_deadline.py | 1 - openpype/modules/deadline/plugins/publish/submit_publish_job.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/modules/deadline/abstract_submit_deadline.py b/openpype/modules/deadline/abstract_submit_deadline.py index 6694f638d6..7938c27233 100644 --- a/openpype/modules/deadline/abstract_submit_deadline.py +++ b/openpype/modules/deadline/abstract_submit_deadline.py @@ -582,7 +582,6 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin): metadata_folder = metadata_folder.replace(orig_scene, new_scene) instance.data["publishRenderMetadataFolder"] = metadata_folder - self.log.debug(f"MetadataFolder:{metadata_folder}") self.log.info("Scene name was switched {} -> {}".format( orig_scene, new_scene )) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 7133cff058..68eb0a437d 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -348,7 +348,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): self.log.info("Submitting Deadline job ...") url = "{}/api/jobs".format(self.deadline_url) - response = requests.post(url, json=payload, timeout=10, verify=False) + response = requests.post(url, json=payload, timeout=10) if not response.ok: raise Exception(response.text) From eb9d8942460f3640c9aeabd63e8fdd45d2e2e955 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 27 May 2023 03:25:05 +0000 Subject: [PATCH 153/189] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index 342bbfc85a..c24388b2ff 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.8" +__version__ = "3.15.9-nightly.1" From f8cb017e90490b80fb6f6470db685090a23e7211 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 27 May 2023 03:25:45 +0000 Subject: [PATCH 154/189] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 4d7d06a2c8..54a4ee6ac0 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.15.9-nightly.1 - 3.15.8 - 3.15.8-nightly.3 - 3.15.8-nightly.2 @@ -134,7 +135,6 @@ body: - 3.14.2-nightly.5 - 3.14.2-nightly.4 - 3.14.2-nightly.3 - - 3.14.2-nightly.2 validations: required: true - type: dropdown From 56642ac17572ca5c7c12bd97e5e717ce801518f0 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 12:37:25 +0800 Subject: [PATCH 155/189] getting the filename from render settings and add save_scene before all the extractors running --- openpype/hosts/max/api/lib_renderproducts.py | 159 ++++++------------ .../hosts/max/plugins/create/create_render.py | 4 + .../max/plugins/publish/collect_render.py | 26 +-- .../hosts/max/plugins/publish/save_scene.py | 26 +++ .../plugins/publish/submit_max_deadline.py | 81 ++++++++- 5 files changed, 171 insertions(+), 125 deletions(-) create mode 100644 openpype/hosts/max/plugins/publish/save_scene.py diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index b33d0c5751..30c3c71cce 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -5,10 +5,8 @@ import os from pymxs import runtime as rt from openpype.hosts.max.api.lib import ( - get_current_renderer, - get_default_render_folder + get_current_renderer ) -from openpype.pipeline.context_tools import get_current_project_asset from openpype.settings import get_project_settings from openpype.pipeline import legacy_io @@ -22,66 +20,30 @@ class RenderProducts(object): legacy_io.Session["AVALON_PROJECT"] ) - def render_product(self, container): - folder = rt.maxFilePath - file = rt.maxFileName - folder = folder.replace("\\", "/") - setting = self._project_settings - render_folder = get_default_render_folder(setting) - filename, ext = os.path.splitext(file) + def get_beauty(self, container): + render_dir = os.path.dirname(rt.rendOutputFilename) - output_file = os.path.join(folder, - render_folder, - filename, + output_file = os.path.join(render_dir, container) - # TODO: change the frame range follows the current render setting + + setting = self._project_settings + img_fmt = setting["max"]["RenderSettings"]["image_format"] # noqa + startFrame = int(rt.rendStart) endFrame = int(rt.rendEnd) + 1 - img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] # noqa - rgba_render_list = self.beauty_render_product(output_file, - startFrame, - endFrame, - img_fmt) - - renderer_class = get_current_renderer() - renderer = str(renderer_class).split(":")[0] - - render_elem_list = None - - if renderer in [ - "ART_Renderer", - "Redshift_Renderer", - "V_Ray_6_Hotfix_3", - "V_Ray_GPU_6_Hotfix_3", - "Default_Scanline_Renderer", - "Quicksilver_Hardware_Renderer", - ]: - render_elem_list = self.render_elements_product(output_file, - startFrame, - endFrame, - img_fmt) - - if renderer == "Arnold": - render_elem_list = self.arnold_render_product(output_file, - startFrame, - endFrame, - img_fmt) - - return rgba_render_list, render_elem_list + render_dict = { + "beauty": self.get_expected_beauty( + output_file, startFrame, endFrame, img_fmt) + } + return render_dict def get_aovs(self, container): - folder = rt.maxFilePath - file = rt.maxFileName - folder = folder.replace("\\", "/") - setting = self._project_settings - render_folder = get_default_render_folder(setting) - filename, ext = os.path.splitext(file) + render_dir = os.path.dirname(rt.rendOutputFilename) - output_file = os.path.join(folder, - render_folder, - filename, + output_file = os.path.join(render_dir, container) + setting = self._project_settings img_fmt = setting["max"]["RenderSettings"]["image_format"] # noqa @@ -90,9 +52,9 @@ class RenderProducts(object): renderer_class = get_current_renderer() renderer = str(renderer_class).split(":")[0] render_dict = {} + if renderer in [ "ART_Renderer", - "Redshift_Renderer", "V_Ray_6_Hotfix_3", "V_Ray_GPU_6_Hotfix_3", "Default_Scanline_Renderer", @@ -105,6 +67,23 @@ class RenderProducts(object): name: self.get_expected_render_elements( output_file, name, startFrame, endFrame, img_fmt) }) + if renderer == "Redshift_Renderer": + render_name = self.get_render_elements_name() + if render_name: + rs_AovFiles = rt.Redshift_Renderer().SeparateAovFiles + if rs_AovFiles != True and img_fmt == "exr": + for name in render_name: + if name == "RsCryptomatte": + render_dict.update({ + name: self.get_expected_render_elements( + output_file, name, startFrame, endFrame, img_fmt) + }) + else: + for name in render_name: + render_dict.update({ + name: self.get_expected_render_elements( + output_file, name, startFrame, endFrame, img_fmt) + }) if renderer == "Arnold": render_name = self.get_arnold_product_name() @@ -114,60 +93,31 @@ class RenderProducts(object): name: self.get_expected_arnold_product( output_file, name, startFrame, endFrame, img_fmt) }) + if renderer in [ + "V_Ray_6_Hotfix_3", + "V_Ray_GPU_6_Hotfix_3" + ]: + if img_fmt !="exr": + render_name = self.get_render_elements_name() + if render_name: + for name in render_name: + render_dict.update({ + name: self.get_expected_render_elements( + output_file, name, startFrame, endFrame, img_fmt) + }) return render_dict - def beauty_render_product(self, folder, startFrame, endFrame, fmt): + def get_expected_beauty(self, folder, startFrame, endFrame, fmt): beauty_frame_range = [] for f in range(startFrame, endFrame): - beauty_output = f"{folder}.{f}.{fmt}" + frame = "%04d" % f + beauty_output = f"{folder}.{frame}.{fmt}" beauty_output = beauty_output.replace("\\", "/") beauty_frame_range.append(beauty_output) return beauty_frame_range - # TODO: Get the arnold render product - def arnold_render_product(self, folder, startFrame, endFrame, fmt): - """Get all the Arnold AOVs""" - aovs = [] - - amw = rt.MaxtoAOps.AOVsManagerWindow() - aov_mgr = rt.renderers.current.AOVManager - # Check if there is any aov group set in AOV manager - aov_group_num = len(aov_mgr.drivers) - if aov_group_num < 1: - return - for i in range(aov_group_num): - # get the specific AOV group - for aov in aov_mgr.drivers[i].aov_list: - for f in range(startFrame, endFrame): - render_element = f"{folder}_{aov.name}.{f}.{fmt}" - render_element = render_element.replace("\\", "/") - aovs.append(render_element) - - # close the AOVs manager window - amw.close() - - return aovs - - def render_elements_product(self, folder, startFrame, endFrame, fmt): - """Get all the render element output files. """ - render_dirname = [] - - render_elem = rt.maxOps.GetCurRenderElementMgr() - render_elem_num = render_elem.NumRenderElements() - # get render elements from the renders - for i in range(render_elem_num): - renderlayer_name = render_elem.GetRenderElement(i) - target, renderpass = str(renderlayer_name).split(":") - if renderlayer_name.enabled: - for f in range(startFrame, endFrame): - render_element = f"{folder}_{renderpass}.{f}.{fmt}" - render_element = render_element.replace("\\", "/") - render_dirname.append(render_element) - - return render_dirname - def get_arnold_product_name(self): """Get all the Arnold AOVs name""" aov_name = [] @@ -193,14 +143,15 @@ class RenderProducts(object): """Get all the expected Arnold AOVs""" aov_list = [] for f in range(startFrame, endFrame): - render_element = f"{folder}_{name}.{f}.{fmt}" + frame = "%04d" % f + render_element = f"{folder}_{name}.{frame}.{fmt}" render_element = render_element.replace("\\", "/") aov_list.append(render_element) return aov_list def get_render_elements_name(self): - """Get all the render element names. """ + """Get all the render element names for general """ render_name = [] render_elem = rt.maxOps.GetCurRenderElementMgr() render_elem_num = render_elem.NumRenderElements() @@ -209,9 +160,10 @@ class RenderProducts(object): # get render elements from the renders for i in range(render_elem_num): renderlayer_name = render_elem.GetRenderElement(i) - if renderlayer_name.enabled or "Cryptomatte" in renderlayer_name: + if renderlayer_name.enabled: target, renderpass = str(renderlayer_name).split(":") render_name.append(renderpass) + return render_name def get_expected_render_elements(self, folder, name, @@ -219,7 +171,8 @@ class RenderProducts(object): """Get all the expected render element output files. """ render_elements = [] for f in range(startFrame, endFrame): - render_element = f"{folder}_{name}.{f}.{fmt}" + frame = "%04d" % f + render_element = f"{folder}_{name}.{frame}.{fmt}" render_element = render_element.replace("\\", "/") render_elements.append(render_element) diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index 68ae5eac72..78e9527bdf 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- """Creator plugin for creating camera.""" +import os from openpype.hosts.max.api import plugin from openpype.pipeline import CreatedInstance from openpype.hosts.max.api.lib_rendersettings import RenderSettings @@ -14,6 +15,9 @@ class CreateRender(plugin.MaxCreator): def create(self, subset_name, instance_data, pre_create_data): from pymxs import runtime as rt sel_obj = list(rt.selection) + file = rt.maxFileName + filename, _ = os.path.splitext(file) + instance_data["AssetName"] = filename instance = super(CreateRender, self).create( subset_name, instance_data, diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index a21ccf532e..5b3f99b2d0 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -29,19 +29,7 @@ class CollectRender(pyblish.api.InstancePlugin): context.data['currentFile'] = current_file asset = get_current_asset_name() - beauty_list, aov_list = RenderProducts().render_product(instance.name) - full_render_list = list() - if aov_list: - full_render_list.extend(iter(beauty_list)) - full_render_list.extend(iter(aov_list)) - - else: - full_render_list = beauty_list - - files_by_aov = { - "beauty": beauty_list - } - + files_by_aov = RenderProducts().get_beauty(instance.name) folder = folder.replace("\\", "/") aovs = RenderProducts().get_aovs(instance.name) files_by_aov.update(aovs) @@ -67,14 +55,14 @@ class CollectRender(pyblish.api.InstancePlugin): # OCIO config not support in # most of the 3dsmax renderers # so this is currently hard coded - setting = instance.context.data["project_settings"] - image_io = setting["global"]["imageio"] - instance.data["colorspaceConfig"] = image_io["ocio_config"]["filepath"][0] # noqa + # TODO: add options for redshift/vray ocio config + instance.data["colorspaceConfig"] = "" instance.data["colorspaceDisplay"] = "sRGB" - instance.data["colorspaceView"] = "ACES 1.0" + instance.data["colorspaceView"] = "ACES 1.0 SDR-video" instance.data["renderProducts"] = colorspace.ARenderProduct() + instance.data["publishJobState"] = "Suspended" instance.data["attachTo"] = [] - + # also need to get the render dir for coversion data = { "asset": asset, "subset": str(instance.name), @@ -84,7 +72,6 @@ class CollectRender(pyblish.api.InstancePlugin): "family": 'maxrender', "families": ['maxrender'], "source": filepath, - "files": full_render_list, "plugin": "3dsmax", "frameStart": int(rt.rendStart), "frameEnd": int(rt.rendEnd), @@ -93,3 +80,4 @@ class CollectRender(pyblish.api.InstancePlugin): } instance.data.update(data) self.log.info("data: {0}".format(data)) + self.log.debug("expectedFiles:{0}".format(instance.data["expectedFiles"])) diff --git a/openpype/hosts/max/plugins/publish/save_scene.py b/openpype/hosts/max/plugins/publish/save_scene.py new file mode 100644 index 0000000000..93d97a3de5 --- /dev/null +++ b/openpype/hosts/max/plugins/publish/save_scene.py @@ -0,0 +1,26 @@ +import pyblish.api +import os + + +class SaveCurrentScene(pyblish.api.ContextPlugin): + """Save current scene + + """ + + label = "Save current file" + order = pyblish.api.ExtractorOrder - 0.49 + hosts = ["max"] + families = ["maxrender", "workfile"] + + def process(self, context): + from pymxs import runtime as rt + folder = rt.maxFilePath + file = rt.maxFileName + current = os.path.join(folder, file) + assert context.data["currentFile"] == current + + if rt.checkForSave(): + self.log.debug("Skipping file save as there " + "are no modifications..") + return + rt.saveMaxFile(current) diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index d2de9160fb..15aa521f43 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -78,7 +78,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, job_info.BatchName = src_filename job_info.Plugin = instance.data["plugin"] job_info.UserName = context.data.get("deadlineUser", getpass.getuser()) - + job_info.EnableAutoTimeout = True # Deadline requires integers in frame range frames = "{start}-{end}".format( start=int(instance.data["frameStart"]), @@ -207,9 +207,13 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, first_file = next(self._iter_expected_files(files)) rgb_bname = os.path.basename(output_beauty) dir = os.path.dirname(first_file) - plugin_data["RenderOutput"] = f"{dir}/{rgb_bname}" - + beauty_name = f"{dir}/{rgb_bname}" + beauty_name = beauty_name.replace("\\", "/") + plugin_data["RenderOutput"] = beauty_name + # as 3dsmax has version with different languages + plugin_data["Language"] = "ENU" renderer_class = get_current_renderer() + renderer = str(renderer_class).split(":")[0] if renderer in [ "ART_Renderer", @@ -223,13 +227,84 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, for i, element in enumerate(render_elem_list): elem_bname = os.path.basename(element) new_elem = f"{dir}/{elem_bname}" + new_elem = new_elem.replace("/", "\\") plugin_data["RenderElementOutputFilename%d" % i] = new_elem # noqa + self.log.debug("plugin data:{}".format(plugin_data)) plugin_info.update(plugin_data) return job_info, plugin_info + def from_published_scene(self, replace_in_path=True): + instance = self._instance + workfile_instance = self._get_workfile_instance(instance.context) + if workfile_instance is None: + return + + # determine published path from Anatomy. + template_data = workfile_instance.data.get("anatomyData") + rep = workfile_instance.data["representations"][0] + template_data["representation"] = rep.get("name") + template_data["ext"] = rep.get("ext") + template_data["comment"] = None + + anatomy = instance.context.data['anatomy'] + template_obj = anatomy.templates_obj["publish"]["path"] + template_filled = template_obj.format_strict(template_data) + file_path = os.path.normpath(template_filled) + + self.log.info("Using published scene for render {}".format(file_path)) + + if not os.path.exists(file_path): + self.log.error("published scene does not exist!") + raise + + if not replace_in_path: + return file_path + + # now we need to switch scene in expected files + # because token will now point to published + # scene file and that might differ from current one + def _clean_name(path): + return os.path.splitext(os.path.basename(path))[0] + + new_scene = _clean_name(file_path) + orig_scene = _clean_name(instance.data["AssetName"]) + expected_files = instance.data.get("expectedFiles") + + if isinstance(expected_files[0], dict): + # we have aovs and we need to iterate over them + new_exp = {} + for aov, files in expected_files[0].items(): + replaced_files = [] + for f in files: + replaced_files.append( + str(f).replace(orig_scene, new_scene) + ) + new_exp[aov] = replaced_files + # [] might be too much here, TODO + instance.data["expectedFiles"] = [new_exp] + else: + new_exp = [] + for f in expected_files: + new_exp.append( + str(f).replace(orig_scene, new_scene) + ) + instance.data["expectedFiles"] = new_exp + + metadata_folder = instance.data.get("publishRenderMetadataFolder") + if metadata_folder: + metadata_folder = metadata_folder.replace(orig_scene, + new_scene) + instance.data["publishRenderMetadataFolder"] = metadata_folder + + self.log.info("Scene name was switched {} -> {}".format( + orig_scene, new_scene + )) + + return file_path + @staticmethod def _iter_expected_files(exp): if isinstance(exp[0], dict): From f85051863ccfbd2f54e0f9198c1b3123f08391b3 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 12:44:22 +0800 Subject: [PATCH 156/189] hound fix --- openpype/hosts/max/api/lib_renderproducts.py | 15 +++++++++------ .../hosts/max/plugins/publish/collect_render.py | 1 - .../plugins/publish/submit_max_deadline.py | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 30c3c71cce..68090dfefd 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -65,7 +65,8 @@ class RenderProducts(object): for name in render_name: render_dict.update({ name: self.get_expected_render_elements( - output_file, name, startFrame, endFrame, img_fmt) + output_file, name, startFrame, + endFrame, img_fmt) }) if renderer == "Redshift_Renderer": render_name = self.get_render_elements_name() @@ -76,13 +77,15 @@ class RenderProducts(object): if name == "RsCryptomatte": render_dict.update({ name: self.get_expected_render_elements( - output_file, name, startFrame, endFrame, img_fmt) + output_file, name, startFrame, + endFrame, img_fmt) }) else: for name in render_name: render_dict.update({ name: self.get_expected_render_elements( - output_file, name, startFrame, endFrame, img_fmt) + output_file, name, startFrame, + endFrame, img_fmt) }) if renderer == "Arnold": @@ -95,15 +98,15 @@ class RenderProducts(object): }) if renderer in [ "V_Ray_6_Hotfix_3", - "V_Ray_GPU_6_Hotfix_3" - ]: + "V_Ray_GPU_6_Hotfix_3"]: if img_fmt !="exr": render_name = self.get_render_elements_name() if render_name: for name in render_name: render_dict.update({ name: self.get_expected_render_elements( - output_file, name, startFrame, endFrame, img_fmt) + output_file, name, startFrame, + endFrame, img_fmt) # noqa }) return render_dict diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 5b3f99b2d0..9137f8c854 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -80,4 +80,3 @@ class CollectRender(pyblish.api.InstancePlugin): } instance.data.update(data) self.log.info("data: {0}".format(data)) - self.log.debug("expectedFiles:{0}".format(instance.data["expectedFiles"])) diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index 15aa521f43..4682cc4487 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -207,7 +207,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, first_file = next(self._iter_expected_files(files)) rgb_bname = os.path.basename(output_beauty) dir = os.path.dirname(first_file) - beauty_name = f"{dir}/{rgb_bname}" + beauty_name = f"{dir}/{rgb_bname}" beauty_name = beauty_name.replace("\\", "/") plugin_data["RenderOutput"] = beauty_name # as 3dsmax has version with different languages From b89bb229a0e7bf4c8d8308776d3d288a65bdd387 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 12:48:09 +0800 Subject: [PATCH 157/189] hound --- openpype/hosts/max/api/lib_renderproducts.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 68090dfefd..21c41446a9 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -72,7 +72,7 @@ class RenderProducts(object): render_name = self.get_render_elements_name() if render_name: rs_AovFiles = rt.Redshift_Renderer().SeparateAovFiles - if rs_AovFiles != True and img_fmt == "exr": + if rs_AovFiles == False and img_fmt == "exr": for name in render_name: if name == "RsCryptomatte": render_dict.update({ @@ -98,7 +98,8 @@ class RenderProducts(object): }) if renderer in [ "V_Ray_6_Hotfix_3", - "V_Ray_GPU_6_Hotfix_3"]: + "V_Ray_GPU_6_Hotfix_3" + ]: if img_fmt !="exr": render_name = self.get_render_elements_name() if render_name: From f030ca17d8a3a7d979f01f3b56b11732d6dcfb85 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 12:50:36 +0800 Subject: [PATCH 158/189] hound --- openpype/hosts/max/api/lib_renderproducts.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 21c41446a9..6bd7e5b7b0 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -72,14 +72,14 @@ class RenderProducts(object): render_name = self.get_render_elements_name() if render_name: rs_AovFiles = rt.Redshift_Renderer().SeparateAovFiles - if rs_AovFiles == False and img_fmt == "exr": + if img_fmt == "exr" and not rs_AovFiles: for name in render_name: if name == "RsCryptomatte": render_dict.update({ - name: self.get_expected_render_elements( - output_file, name, startFrame, - endFrame, img_fmt) - }) + name: self.get_expected_render_elements( + output_file, name, startFrame, + endFrame, img_fmt) + }) else: for name in render_name: render_dict.update({ @@ -99,7 +99,7 @@ class RenderProducts(object): if renderer in [ "V_Ray_6_Hotfix_3", "V_Ray_GPU_6_Hotfix_3" - ]: + ]: if img_fmt !="exr": render_name = self.get_render_elements_name() if render_name: From 44c0f1cf8a52a5eb1b45d43310f2451aa966be01 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 12:51:26 +0800 Subject: [PATCH 159/189] hound --- openpype/hosts/max/api/lib_renderproducts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 6bd7e5b7b0..2fbb7e8ff3 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -100,7 +100,7 @@ class RenderProducts(object): "V_Ray_6_Hotfix_3", "V_Ray_GPU_6_Hotfix_3" ]: - if img_fmt !="exr": + if img_fmt != "exr": render_name = self.get_render_elements_name() if render_name: for name in render_name: From 0fccdaf5f5d46e775863193ff7f3a44e3338eda9 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 15:54:55 +0800 Subject: [PATCH 160/189] use expected files instead of files --- .../modules/deadline/plugins/publish/submit_max_deadline.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index 4682cc4487..3fde667dfe 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -197,10 +197,11 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, else: plugin_data["DisableMultipass"] = 1 - files = instance.data.get("files") + files = instance.data.get("expectedFiles") if not files: raise RuntimeError("No render elements found") - old_output_dir = os.path.dirname(files[0]) + first_file = next(self._iter_expected_files(files)) + old_output_dir = os.path.dirname(first_file) output_beauty = RenderSettings().get_render_output(instance.name, old_output_dir) files = instance.data["expectedFiles"] From 66070377104142edce133201389e896774d1f3f9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 29 May 2023 11:25:14 +0200 Subject: [PATCH 161/189] Publisher: Call explicitly prepared tab methods (#5044) * call explicitly prepared tab methods * add an overlay message --- openpype/tools/publisher/window.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index fc90e66f21..6ab444109e 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -676,7 +676,15 @@ class PublisherWindow(QtWidgets.QDialog): self._tabs_widget.set_current_tab(identifier) def set_current_tab(self, tab): - self._set_current_tab(tab) + if tab == "create": + self._go_to_create_tab() + elif tab == "publish": + self._go_to_publish_tab() + elif tab == "report": + self._go_to_report_tab() + elif tab == "details": + self._go_to_details_tab() + if not self._window_is_visible: self.set_tab_on_reset(tab) @@ -686,6 +694,12 @@ class PublisherWindow(QtWidgets.QDialog): def _go_to_create_tab(self): if self._create_tab.isEnabled(): self._set_current_tab("create") + return + + self._overlay_object.add_message( + "Can't switch to Create tab because publishing is paused.", + message_type="info" + ) def _go_to_publish_tab(self): self._set_current_tab("publish") From a4fd3c7c6dae31509fc37621878aa4600933f2ae Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 29 May 2023 11:50:56 +0100 Subject: [PATCH 162/189] Minor refactor --- .../hosts/unreal/plugins/load/load_camera.py | 45 +++++++------------ 1 file changed, 15 insertions(+), 30 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index c4fe9df70b..af7a594b41 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -89,10 +89,7 @@ class CameraLoader(plugin.Loader): hierarchy_dir_list.append(hierarchy_dir) asset = context.get('asset').get('name') suffix = "_CON" - if asset: - asset_name = "{}_{}".format(asset, name) - else: - asset_name = "{}".format(name) + asset_name = f"{asset}_{name}" if asset else f"{name}" tools = unreal.AssetToolsHelpers().get_asset_tools() @@ -106,23 +103,15 @@ class CameraLoader(plugin.Loader): # Get highest number to make a unique name folders = [a for a in asset_content if a[-1] == "/" and f"{name}_" in a] - f_numbers = [] - for f in folders: - # Get number from folder name. Splits the string by "_" and - # removes the last element (which is a "/"). - f_numbers.append(int(f.split("_")[-1][:-1])) + # Get number from folder name. Splits the string by "_" and + # removes the last element (which is a "/"). + f_numbers = [int(f.split("_")[-1][:-1]) for f in folders] f_numbers.sort() - if not f_numbers: - unique_number = 1 - else: - unique_number = f_numbers[-1] + 1 + unique_number = f_numbers[-1] + 1 if f_numbers else 1 asset_dir, container_name = tools.create_unique_asset_name( f"{hierarchy_dir}/{asset}/{name}_{unique_number:02d}", suffix="") - asset_path = Path(asset_dir) - asset_path_parent = str(asset_path.parent.as_posix()) - container_name += suffix EditorAssetLibrary.make_directory(asset_dir) @@ -163,17 +152,17 @@ class CameraLoader(plugin.Loader): asset).get_class().get_name() == 'LevelSequence' ] - if not existing_sequences: + if existing_sequences: + for seq in existing_sequences: + sequences.append(seq.get_asset()) + frame_ranges.append(( + seq.get_asset().get_playback_start(), + seq.get_asset().get_playback_end())) + else: sequence, frame_range = generate_sequence(h, h_dir) sequences.append(sequence) frame_ranges.append(frame_range) - else: - for e in existing_sequences: - sequences.append(e.get_asset()) - frame_ranges.append(( - e.get_asset().get_playback_start(), - e.get_asset().get_playback_end())) EditorAssetLibrary.make_directory(asset_dir) @@ -252,8 +241,7 @@ class CameraLoader(plugin.Loader): "parent": context["representation"]["parent"], "family": context["representation"]["context"]["family"] } - imprint( - "{}/{}".format(asset_dir, container_name), data) + imprint(f"{asset_dir}/{container_name}", data) EditorLevelLibrary.save_all_dirty_levels() EditorLevelLibrary.load_level(master_level) @@ -415,8 +403,7 @@ class CameraLoader(plugin.Loader): "representation": str(representation["_id"]), "parent": str(representation["parent"]) } - imprint( - "{}/{}".format(asset_dir, container.get('container_name')), data) + imprint(f"{asset_dir}/{container.get('container_name')}", data) EditorLevelLibrary.save_current_level() @@ -514,10 +501,8 @@ class CameraLoader(plugin.Loader): break sequences.append(ss.get_sequence()) # Update subscenes indexes. - i = 0 - for ss in sections: + for i, ss in enumerate(sections): ss.set_row_index(i) - i += 1 if visibility_track: sections = visibility_track.get_sections() From 79a0210c35eda89c355afd0497d2ae14fa9bcb45 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 29 May 2023 11:56:03 +0100 Subject: [PATCH 163/189] Save whole hierarchy when loading camera or layout --- openpype/hosts/unreal/plugins/load/load_camera.py | 5 +++-- openpype/hosts/unreal/plugins/load/load_layout.py | 11 ++++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index af7a594b41..3ed7b055e3 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -246,8 +246,9 @@ class CameraLoader(plugin.Loader): EditorLevelLibrary.save_all_dirty_levels() EditorLevelLibrary.load_level(master_level) + # Save all assets in the hierarchy asset_content = EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=True + hierarchy_dir_list[0], recursive=True, include_folder=False ) for a in asset_content: @@ -408,7 +409,7 @@ class CameraLoader(plugin.Loader): EditorLevelLibrary.save_current_level() asset_content = EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=False) + f"{root}/{ms_asset}", recursive=True, include_folder=False) for a in asset_content: EditorAssetLibrary.save_asset(a) diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index 5a3953f82e..51ca0383e0 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -642,8 +642,10 @@ class LayoutLoader(plugin.Loader): imprint( "{}/{}".format(asset_dir, container_name), data) + save_dir = hierarchy_dir_list[0] if create_sequences else asset_dir + asset_content = EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=False) + save_dir, recursive=True, include_folder=False) for a in asset_content: EditorAssetLibrary.save_asset(a) @@ -664,11 +666,12 @@ class LayoutLoader(plugin.Loader): asset_dir = container.get('namespace') context = representation.get("context") + hierarchy = context.get('hierarchy').split("/") + sequence = None master_level = None if create_sequences: - hierarchy = context.get('hierarchy').split("/") h_dir = f"{root}/{hierarchy[0]}" h_asset = hierarchy[0] master_level = f"{h_dir}/{h_asset}_map.{h_asset}_map" @@ -726,8 +729,10 @@ class LayoutLoader(plugin.Loader): EditorLevelLibrary.save_current_level() + save_dir = f"{root}/{hierarchy[0]}" if create_sequences else asset_dir + asset_content = EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=False) + save_dir, recursive=True, include_folder=False) for a in asset_content: EditorAssetLibrary.save_asset(a) From ff2c494705f79d21b75dcfc77fb2c1a49b23bae8 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 29 May 2023 12:10:23 +0100 Subject: [PATCH 164/189] Set view range in sequencer when creating sequences --- openpype/hosts/unreal/api/pipeline.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/api/pipeline.py b/openpype/hosts/unreal/api/pipeline.py index 5030e8ee86..72816c9b81 100644 --- a/openpype/hosts/unreal/api/pipeline.py +++ b/openpype/hosts/unreal/api/pipeline.py @@ -623,11 +623,18 @@ def generate_sequence(h, h_dir): min_frame = min(start_frames) max_frame = max(end_frames) + fps = asset_data.get('data').get("fps") + sequence.set_display_rate( - unreal.FrameRate(asset_data.get('data').get("fps"), 1.0)) + unreal.FrameRate(fps, 1.0)) sequence.set_playback_start(min_frame) sequence.set_playback_end(max_frame) + sequence.set_work_range_start(min_frame / fps) + sequence.set_work_range_end(max_frame / fps) + sequence.set_view_range_start(min_frame / fps) + sequence.set_view_range_end(max_frame / fps) + tracks = sequence.get_master_tracks() track = None for t in tracks: From 9d9eebfe09499474dac3504d7c4b252100e8c57d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 19:25:49 +0800 Subject: [PATCH 165/189] using current file to render and add validator for deadline publish in max hosts --- openpype/hosts/max/api/lib_renderproducts.py | 4 +- .../hosts/max/plugins/create/create_render.py | 14 ++++ .../max/plugins/publish/collect_render.py | 9 ++- .../hosts/max/plugins/publish/save_scene.py | 5 -- .../publish/validate_deadline_publish.py | 41 ++++++++++ .../plugins/publish/submit_max_deadline.py | 76 ++----------------- 6 files changed, 72 insertions(+), 77 deletions(-) create mode 100644 openpype/hosts/max/plugins/publish/validate_deadline_publish.py diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 2fbb7e8ff3..7e016f6f15 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -38,7 +38,7 @@ class RenderProducts(object): } return render_dict - def get_aovs(self, container): + def get_aovs(self, container, instance): render_dir = os.path.dirname(rt.rendOutputFilename) output_file = os.path.join(render_dir, @@ -71,7 +71,7 @@ class RenderProducts(object): if renderer == "Redshift_Renderer": render_name = self.get_render_elements_name() if render_name: - rs_AovFiles = rt.Redshift_Renderer().SeparateAovFiles + rs_AovFiles = instance.data.get("separateAovFiles") if img_fmt == "exr" and not rs_AovFiles: for name in render_name: if name == "RsCryptomatte": diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index 78e9527bdf..3d5ed781a4 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -2,6 +2,7 @@ """Creator plugin for creating camera.""" import os from openpype.hosts.max.api import plugin +from openpype.lib import BoolDef from openpype.pipeline import CreatedInstance from openpype.hosts.max.api.lib_rendersettings import RenderSettings @@ -18,6 +19,9 @@ class CreateRender(plugin.MaxCreator): file = rt.maxFileName filename, _ = os.path.splitext(file) instance_data["AssetName"] = filename + instance_data["separateAovFiles"] = ( + pre_create_data.get("separateAovFiles")) + instance = super(CreateRender, self).create( subset_name, instance_data, @@ -40,3 +44,13 @@ class CreateRender(plugin.MaxCreator): RenderSettings().set_render_camera(sel_obj) # set output paths for rendering(mandatory for deadline) RenderSettings().render_output(container_name) + + def get_pre_create_attr_defs(self): + attrs = super(CreateRender, self).get_pre_create_attr_defs() + + return attrs + [ + BoolDef("separateAovFiles", + label="Separate Aov Files", + default=False), + + ] diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 9137f8c854..14d23c42fc 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -6,7 +6,7 @@ import pyblish.api from pymxs import runtime as rt from openpype.pipeline import get_current_asset_name from openpype.hosts.max.api import colorspace -from openpype.hosts.max.api.lib import get_max_version +from openpype.hosts.max.api.lib import get_max_version, get_current_renderer from openpype.hosts.max.api.lib_renderproducts import RenderProducts from openpype.client import get_last_version_by_subset_name @@ -31,12 +31,14 @@ class CollectRender(pyblish.api.InstancePlugin): files_by_aov = RenderProducts().get_beauty(instance.name) folder = folder.replace("\\", "/") - aovs = RenderProducts().get_aovs(instance.name) + aovs = RenderProducts().get_aovs(instance.name, instance) files_by_aov.update(aovs) if "expectedFiles" not in instance.data: instance.data["expectedFiles"] = list() + instance.data["files"] = list() instance.data["expectedFiles"].append(files_by_aov) + instance.data["files"].append(files_by_aov) img_format = RenderProducts().image_format() project_name = context.data["projectName"] @@ -62,6 +64,8 @@ class CollectRender(pyblish.api.InstancePlugin): instance.data["renderProducts"] = colorspace.ARenderProduct() instance.data["publishJobState"] = "Suspended" instance.data["attachTo"] = [] + renderer_class = get_current_renderer() + renderer = str(renderer_class).split(":")[0] # also need to get the render dir for coversion data = { "asset": asset, @@ -71,6 +75,7 @@ class CollectRender(pyblish.api.InstancePlugin): "imageFormat": img_format, "family": 'maxrender', "families": ['maxrender'], + "renderer": renderer, "source": filepath, "plugin": "3dsmax", "frameStart": int(rt.rendStart), diff --git a/openpype/hosts/max/plugins/publish/save_scene.py b/openpype/hosts/max/plugins/publish/save_scene.py index 93d97a3de5..a40788ab41 100644 --- a/openpype/hosts/max/plugins/publish/save_scene.py +++ b/openpype/hosts/max/plugins/publish/save_scene.py @@ -18,9 +18,4 @@ class SaveCurrentScene(pyblish.api.ContextPlugin): file = rt.maxFileName current = os.path.join(folder, file) assert context.data["currentFile"] == current - - if rt.checkForSave(): - self.log.debug("Skipping file save as there " - "are no modifications..") - return rt.saveMaxFile(current) diff --git a/openpype/hosts/max/plugins/publish/validate_deadline_publish.py b/openpype/hosts/max/plugins/publish/validate_deadline_publish.py new file mode 100644 index 0000000000..f516e09337 --- /dev/null +++ b/openpype/hosts/max/plugins/publish/validate_deadline_publish.py @@ -0,0 +1,41 @@ +import os +import pyblish.api +from pymxs import runtime as rt +from openpype.pipeline.publish import ( + RepairAction, + ValidateContentsOrder, + PublishValidationError, + OptionalPyblishPluginMixin +) +from openpype.hosts.max.api.lib_rendersettings import RenderSettings + + +class ValidateDeadlinePublish(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): + """Validates Render File Directory is + not the same in every submission + """ + + order = ValidateContentsOrder + families = ["maxrender"] + hosts = ["max"] + label = "Render Output for Deadline" + optional = True + actions = [RepairAction] + + def process(self, instance): + if not self.is_active(instance.data): + return + file = rt.maxFileName + filename, ext = os.path.splitext(file) + if filename not in rt.rendOutputFilename: + raise PublishValidationError( + "Directory of RenderOutput doesn't " + "have with the current Max SceneName " + "please repair to rename the folder!" + ) + + @classmethod + def repair(cls, instance): + container= instance.data.get("instance_node") + RenderSettings().render_output(container) diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index 3fde667dfe..d7ba7107a3 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -133,6 +133,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, # Add list of expected files to job # --------------------------------- exp = instance.data.get("expectedFiles") + for filepath in self._iter_expected_files(exp): job_info.OutputDirectory += os.path.dirname(filepath) job_info.OutputFilename += os.path.basename(filepath) @@ -204,8 +205,6 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, old_output_dir = os.path.dirname(first_file) output_beauty = RenderSettings().get_render_output(instance.name, old_output_dir) - files = instance.data["expectedFiles"] - first_file = next(self._iter_expected_files(files)) rgb_bname = os.path.basename(output_beauty) dir = os.path.dirname(first_file) beauty_name = f"{dir}/{rgb_bname}" @@ -231,6 +230,9 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, new_elem = new_elem.replace("/", "\\") plugin_data["RenderElementOutputFilename%d" % i] = new_elem # noqa + if renderer == "Redshift_Renderer": + plugin_data["redshift_SeparateAovFiles"] = instance.data.get( + "separateAovFiles", False) self.log.debug("plugin data:{}".format(plugin_data)) plugin_info.update(plugin_data) @@ -239,72 +241,10 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, def from_published_scene(self, replace_in_path=True): instance = self._instance - workfile_instance = self._get_workfile_instance(instance.context) - if workfile_instance is None: - return - - # determine published path from Anatomy. - template_data = workfile_instance.data.get("anatomyData") - rep = workfile_instance.data["representations"][0] - template_data["representation"] = rep.get("name") - template_data["ext"] = rep.get("ext") - template_data["comment"] = None - - anatomy = instance.context.data['anatomy'] - template_obj = anatomy.templates_obj["publish"]["path"] - template_filled = template_obj.format_strict(template_data) - file_path = os.path.normpath(template_filled) - - self.log.info("Using published scene for render {}".format(file_path)) - - if not os.path.exists(file_path): - self.log.error("published scene does not exist!") - raise - - if not replace_in_path: - return file_path - - # now we need to switch scene in expected files - # because token will now point to published - # scene file and that might differ from current one - def _clean_name(path): - return os.path.splitext(os.path.basename(path))[0] - - new_scene = _clean_name(file_path) - orig_scene = _clean_name(instance.data["AssetName"]) - expected_files = instance.data.get("expectedFiles") - - if isinstance(expected_files[0], dict): - # we have aovs and we need to iterate over them - new_exp = {} - for aov, files in expected_files[0].items(): - replaced_files = [] - for f in files: - replaced_files.append( - str(f).replace(orig_scene, new_scene) - ) - new_exp[aov] = replaced_files - # [] might be too much here, TODO - instance.data["expectedFiles"] = [new_exp] - else: - new_exp = [] - for f in expected_files: - new_exp.append( - str(f).replace(orig_scene, new_scene) - ) - instance.data["expectedFiles"] = new_exp - - metadata_folder = instance.data.get("publishRenderMetadataFolder") - if metadata_folder: - metadata_folder = metadata_folder.replace(orig_scene, - new_scene) - instance.data["publishRenderMetadataFolder"] = metadata_folder - - self.log.info("Scene name was switched {} -> {}".format( - orig_scene, new_scene - )) - - return file_path + if instance.data["renderer"] == "Redshift_Renderer": + self.log.debug("Using Redshift...published scene wont be used..") + replace_in_path = False + return replace_in_path @staticmethod def _iter_expected_files(exp): From dbb175204adf16e763d2c2295d6ad675988453ba Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 19:29:35 +0800 Subject: [PATCH 166/189] hound --- openpype/hosts/max/plugins/publish/validate_deadline_publish.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/validate_deadline_publish.py b/openpype/hosts/max/plugins/publish/validate_deadline_publish.py index f516e09337..c5bc979043 100644 --- a/openpype/hosts/max/plugins/publish/validate_deadline_publish.py +++ b/openpype/hosts/max/plugins/publish/validate_deadline_publish.py @@ -37,5 +37,5 @@ class ValidateDeadlinePublish(pyblish.api.InstancePlugin, @classmethod def repair(cls, instance): - container= instance.data.get("instance_node") + container = instance.data.get("instance_node") RenderSettings().render_output(container) From eb5b5bf492c69d3369d944ff019f36a6e306b3bd Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 29 May 2023 12:59:54 +0100 Subject: [PATCH 167/189] Set back sequencer and viewport when updating layout or camera --- .../hosts/unreal/plugins/load/load_camera.py | 23 ++++++++++++-- .../hosts/unreal/plugins/load/load_layout.py | 31 ++++++++++++++----- 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 3ed7b055e3..59ea14697d 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -3,9 +3,12 @@ from pathlib import Path import unreal -from unreal import EditorAssetLibrary -from unreal import EditorLevelLibrary -from unreal import EditorLevelUtils +from unreal import ( + EditorAssetLibrary, + EditorLevelLibrary, + EditorLevelUtils, + LevelSequenceEditorBlueprintLibrary as LevelSequenceLib, +) from openpype.client import get_asset_by_name from openpype.pipeline import ( AYON_CONTAINER_ID, @@ -259,6 +262,13 @@ class CameraLoader(plugin.Loader): def update(self, container, representation): ar = unreal.AssetRegistryHelpers.get_asset_registry() + curr_level_sequence = LevelSequenceLib.get_current_level_sequence() + curr_time = LevelSequenceLib.get_current_time() + is_cam_lock = LevelSequenceLib.is_camera_cut_locked_to_viewport() + + editor_subsystem = unreal.UnrealEditorSubsystem() + vp_loc, vp_rot = editor_subsystem.get_level_viewport_camera_info() + asset_dir = container.get('namespace') EditorLevelLibrary.save_current_level() @@ -416,6 +426,13 @@ class CameraLoader(plugin.Loader): EditorLevelLibrary.load_level(master_level) + if curr_level_sequence: + LevelSequenceLib.open_level_sequence(curr_level_sequence) + LevelSequenceLib.set_current_time(curr_time) + LevelSequenceLib.set_lock_camera_cut_to_viewport(is_cam_lock) + + editor_subsystem.set_level_viewport_camera_info(vp_loc, vp_rot) + def remove(self, container): asset_dir = container.get('namespace') path = Path(asset_dir) diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index 51ca0383e0..86b2e1456c 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -5,13 +5,16 @@ import collections from pathlib import Path import unreal -from unreal import EditorAssetLibrary -from unreal import EditorLevelLibrary -from unreal import EditorLevelUtils -from unreal import AssetToolsHelpers -from unreal import FBXImportType -from unreal import MovieSceneLevelVisibilityTrack -from unreal import MovieSceneSubTrack +from unreal import ( + EditorAssetLibrary, + EditorLevelLibrary, + EditorLevelUtils, + AssetToolsHelpers, + FBXImportType, + MovieSceneLevelVisibilityTrack, + MovieSceneSubTrack, + LevelSequenceEditorBlueprintLibrary as LevelSequenceLib, +) from openpype.client import get_asset_by_name, get_representations from openpype.pipeline import ( @@ -661,6 +664,13 @@ class LayoutLoader(plugin.Loader): ar = unreal.AssetRegistryHelpers.get_asset_registry() + curr_level_sequence = LevelSequenceLib.get_current_level_sequence() + curr_time = LevelSequenceLib.get_current_time() + is_cam_lock = LevelSequenceLib.is_camera_cut_locked_to_viewport() + + editor_subsystem = unreal.UnrealEditorSubsystem() + vp_loc, vp_rot = editor_subsystem.get_level_viewport_camera_info() + root = "/Game/Ayon" asset_dir = container.get('namespace') @@ -742,6 +752,13 @@ class LayoutLoader(plugin.Loader): elif prev_level: EditorLevelLibrary.load_level(prev_level) + if curr_level_sequence: + LevelSequenceLib.open_level_sequence(curr_level_sequence) + LevelSequenceLib.set_current_time(curr_time) + LevelSequenceLib.set_lock_camera_cut_to_viewport(is_cam_lock) + + editor_subsystem.set_level_viewport_camera_info(vp_loc, vp_rot) + def remove(self, container): """ Delete the layout. First, check if the assets loaded with the layout From e506d88ed3d113f608759e883dcdf17dcb6f5ccc Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 20:33:40 +0800 Subject: [PATCH 168/189] style fix --- openpype/hosts/max/plugins/create/create_render.py | 4 +--- .../modules/deadline/plugins/publish/submit_max_deadline.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index 3d5ed781a4..6c581cdcf4 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -47,10 +47,8 @@ class CreateRender(plugin.MaxCreator): def get_pre_create_attr_defs(self): attrs = super(CreateRender, self).get_pre_create_attr_defs() - return attrs + [ BoolDef("separateAovFiles", label="Separate Aov Files", - default=False), - + default=False) ] diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index d7ba7107a3..b6a30e36b7 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -232,7 +232,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, if renderer == "Redshift_Renderer": plugin_data["redshift_SeparateAovFiles"] = instance.data.get( - "separateAovFiles", False) + "separateAovFiles") self.log.debug("plugin data:{}".format(plugin_data)) plugin_info.update(plugin_data) From 5f763a4e8e65d0aeb8e4515e69ec768f6f2c5f4d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 20:36:38 +0800 Subject: [PATCH 169/189] dont add separate aov as instance data --- openpype/hosts/max/api/lib_renderproducts.py | 4 ++-- openpype/hosts/max/plugins/create/create_render.py | 10 ---------- openpype/hosts/max/plugins/publish/collect_render.py | 2 +- 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 7e016f6f15..a6427bf7c5 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -38,7 +38,7 @@ class RenderProducts(object): } return render_dict - def get_aovs(self, container, instance): + def get_aovs(self, container): render_dir = os.path.dirname(rt.rendOutputFilename) output_file = os.path.join(render_dir, @@ -71,7 +71,7 @@ class RenderProducts(object): if renderer == "Redshift_Renderer": render_name = self.get_render_elements_name() if render_name: - rs_AovFiles = instance.data.get("separateAovFiles") + rs_AovFiles = rt.RedShift_Renderer().separateAovFiles if img_fmt == "exr" and not rs_AovFiles: for name in render_name: if name == "RsCryptomatte": diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index 6c581cdcf4..be5dece05b 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -19,8 +19,6 @@ class CreateRender(plugin.MaxCreator): file = rt.maxFileName filename, _ = os.path.splitext(file) instance_data["AssetName"] = filename - instance_data["separateAovFiles"] = ( - pre_create_data.get("separateAovFiles")) instance = super(CreateRender, self).create( subset_name, @@ -44,11 +42,3 @@ class CreateRender(plugin.MaxCreator): RenderSettings().set_render_camera(sel_obj) # set output paths for rendering(mandatory for deadline) RenderSettings().render_output(container_name) - - def get_pre_create_attr_defs(self): - attrs = super(CreateRender, self).get_pre_create_attr_defs() - return attrs + [ - BoolDef("separateAovFiles", - label="Separate Aov Files", - default=False) - ] diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 14d23c42fc..77ab5f654d 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -31,7 +31,7 @@ class CollectRender(pyblish.api.InstancePlugin): files_by_aov = RenderProducts().get_beauty(instance.name) folder = folder.replace("\\", "/") - aovs = RenderProducts().get_aovs(instance.name, instance) + aovs = RenderProducts().get_aovs(instance.name) files_by_aov.update(aovs) if "expectedFiles" not in instance.data: From 982186ffa08618f8d590e894356595f886af0f57 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 20:38:17 +0800 Subject: [PATCH 170/189] remove unused import --- openpype/hosts/max/plugins/create/create_render.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index be5dece05b..5ad895b86e 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -2,7 +2,6 @@ """Creator plugin for creating camera.""" import os from openpype.hosts.max.api import plugin -from openpype.lib import BoolDef from openpype.pipeline import CreatedInstance from openpype.hosts.max.api.lib_rendersettings import RenderSettings From bafc085ec19e624c163d6a862b9fcc6e0edab983 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 22:56:47 +0800 Subject: [PATCH 171/189] updating validator's comment --- .../max/plugins/publish/validate_deadline_publish.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_deadline_publish.py b/openpype/hosts/max/plugins/publish/validate_deadline_publish.py index c5bc979043..b2f0e863f4 100644 --- a/openpype/hosts/max/plugins/publish/validate_deadline_publish.py +++ b/openpype/hosts/max/plugins/publish/validate_deadline_publish.py @@ -30,12 +30,14 @@ class ValidateDeadlinePublish(pyblish.api.InstancePlugin, filename, ext = os.path.splitext(file) if filename not in rt.rendOutputFilename: raise PublishValidationError( - "Directory of RenderOutput doesn't " - "have with the current Max SceneName " - "please repair to rename the folder!" + "Render output folder " + "doesn't match the max scene name! " + "Use Repair action to " + "fix the folder file path.." ) @classmethod def repair(cls, instance): container = instance.data.get("instance_node") RenderSettings().render_output(container) + cls.log.debug("Reset the render output folder...") From 5fbae39a745b0016b2ada9d054b696aef2007fee Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 30 May 2023 13:32:27 +0200 Subject: [PATCH 172/189] Ftrack: Role names are not case sensitive in ftrack event server status action (#5058) * statuser is not case sensitive about role names * safer role check --- .../ftrack/lib/ftrack_action_handler.py | 23 +++++++++++++++---- .../ftrack/scripts/sub_event_status.py | 4 ++-- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/openpype/modules/ftrack/lib/ftrack_action_handler.py b/openpype/modules/ftrack/lib/ftrack_action_handler.py index 07b3a780a2..1be4353b26 100644 --- a/openpype/modules/ftrack/lib/ftrack_action_handler.py +++ b/openpype/modules/ftrack/lib/ftrack_action_handler.py @@ -234,6 +234,10 @@ class BaseAction(BaseHandler): if not settings_roles: return default + user_roles = { + role_name.lower() + for role_name in user_roles + } for role_name in settings_roles: if role_name.lower() in user_roles: return True @@ -264,8 +268,15 @@ class BaseAction(BaseHandler): return user_entity @classmethod - def get_user_roles_from_event(cls, session, event): - """Query user entity from event.""" + def get_user_roles_from_event(cls, session, event, lower=True): + """Get user roles based on data in event. + + Args: + session (ftrack_api.Session): Prepared ftrack session. + event (ftrack_api.event.Event): Event which is processed. + lower (Optional[bool]): Lower the role names. Default 'True'. + """ + not_set = object() user_roles = event["data"].get("user_roles", not_set) @@ -273,7 +284,10 @@ class BaseAction(BaseHandler): user_roles = [] user_entity = cls.get_user_entity_from_event(session, event) for role in user_entity["user_security_roles"]: - user_roles.append(role["security_role"]["name"].lower()) + role_name = role["security_role"]["name"] + if lower: + role_name = role_name.lower() + user_roles.append(role_name) event["data"]["user_roles"] = user_roles return user_roles @@ -322,7 +336,8 @@ class BaseAction(BaseHandler): if not settings.get(self.settings_enabled_key, True): return False - user_role_list = self.get_user_roles_from_event(session, event) + user_role_list = self.get_user_roles_from_event( + session, event, lower=False) if not self.roles_check(settings.get("role_list"), user_role_list): return False return True diff --git a/openpype/modules/ftrack/scripts/sub_event_status.py b/openpype/modules/ftrack/scripts/sub_event_status.py index dc5836e7f2..c6c2e9e1f6 100644 --- a/openpype/modules/ftrack/scripts/sub_event_status.py +++ b/openpype/modules/ftrack/scripts/sub_event_status.py @@ -296,9 +296,9 @@ def server_activity_validate_user(event): if not user_ent: return False - role_list = ["Pypeclub", "Administrator"] + role_list = {"pypeclub", "administrator"} for role in user_ent["user_security_roles"]: - if role["security_role"]["name"] in role_list: + if role["security_role"]["name"].lower() in role_list: return True return False From 4bc61d1c89f1c3adfbbc7ba80fa2fff5f156eec0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 30 May 2023 13:33:58 +0200 Subject: [PATCH 173/189] Fix border widget --- .../publisher/widgets/border_label_widget.py | 125 +++++++++++++----- 1 file changed, 94 insertions(+), 31 deletions(-) diff --git a/openpype/tools/publisher/widgets/border_label_widget.py b/openpype/tools/publisher/widgets/border_label_widget.py index 5617e159cd..1381d74eb3 100644 --- a/openpype/tools/publisher/widgets/border_label_widget.py +++ b/openpype/tools/publisher/widgets/border_label_widget.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- +from math import ceil from qtpy import QtWidgets, QtCore, QtGui from openpype.style import get_objected_colors @@ -14,32 +15,44 @@ class _VLineWidget(QtWidgets.QWidget): It is expected that parent widget will set width. """ - def __init__(self, color, left, parent): + def __init__(self, color, line_size, left, parent): super(_VLineWidget, self).__init__(parent) self._color = color self._left = left + self._line_size = line_size + + def set_line_size(self, line_size): + self._line_size = line_size def paintEvent(self, event): if not self.isVisible(): return - if self._left: - pos_x = 0 - else: - pos_x = self.width() + pos_x = self._line_size * 0.5 + if not self._left: + pos_x = self.width() - pos_x + painter = QtGui.QPainter(self) painter.setRenderHints( QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform ) + if self._color: pen = QtGui.QPen(self._color) else: pen = painter.pen() - pen.setWidth(1) + pen.setWidth(self._line_size) painter.setPen(pen) painter.setBrush(QtCore.Qt.transparent) - painter.drawLine(pos_x, 0, pos_x, self.height()) + painter.drawRect( + QtCore.QRectF( + pos_x, + -self._line_size, + pos_x + (self.width() * 2), + self.height() + (self._line_size * 2) + ) + ) painter.end() @@ -56,34 +69,46 @@ class _HBottomLineWidget(QtWidgets.QWidget): It is expected that parent widget will set height and radius. """ - def __init__(self, color, parent): + def __init__(self, color, line_size, parent): super(_HBottomLineWidget, self).__init__(parent) self._color = color self._radius = 0 + self._line_size = line_size def set_radius(self, radius): self._radius = radius + def set_line_size(self, line_size): + self._line_size = line_size + def paintEvent(self, event): if not self.isVisible(): return - rect = QtCore.QRect( - 0, -self._radius, self.width(), self.height() + self._radius + x_offset = self._line_size * 0.5 + rect = QtCore.QRectF( + x_offset, + -self._radius, + self.width() - (2 * x_offset), + (self.height() + self._radius) - x_offset ) painter = QtGui.QPainter(self) painter.setRenderHints( QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform ) + if self._color: pen = QtGui.QPen(self._color) else: pen = painter.pen() - pen.setWidth(1) + pen.setWidth(self._line_size) painter.setPen(pen) painter.setBrush(QtCore.Qt.transparent) - painter.drawRoundedRect(rect, self._radius, self._radius) + if self._radius: + painter.drawRoundedRect(rect, self._radius, self._radius) + else: + painter.drawRect(rect) painter.end() @@ -102,30 +127,38 @@ class _HTopCornerLineWidget(QtWidgets.QWidget): It is expected that parent widget will set height and radius. """ - def __init__(self, color, left_side, parent): + + def __init__(self, color, line_size, left_side, parent): super(_HTopCornerLineWidget, self).__init__(parent) self._left_side = left_side + self._line_size = line_size self._color = color self._radius = 0 def set_radius(self, radius): self._radius = radius + def set_line_size(self, line_size): + self._line_size = line_size + def paintEvent(self, event): if not self.isVisible(): return - pos_y = self.height() / 2 - + pos_y = self.height() * 0.5 + x_offset = self._line_size * 0.5 if self._left_side: - rect = QtCore.QRect( - 0, pos_y, self.width() + self._radius, self.height() + rect = QtCore.QRectF( + x_offset, + pos_y, + self.width() + self._radius + x_offset, + self.height() ) else: - rect = QtCore.QRect( - -self._radius, + rect = QtCore.QRectF( + (-self._radius), pos_y, - self.width() + self._radius, + (self.width() + self._radius) - x_offset, self.height() ) @@ -138,10 +171,13 @@ class _HTopCornerLineWidget(QtWidgets.QWidget): pen = QtGui.QPen(self._color) else: pen = painter.pen() - pen.setWidth(1) + pen.setWidth(self._line_size) painter.setPen(pen) painter.setBrush(QtCore.Qt.transparent) - painter.drawRoundedRect(rect, self._radius, self._radius) + if self._radius: + painter.drawRoundedRect(rect, self._radius, self._radius) + else: + painter.drawRect(rect) painter.end() @@ -163,8 +199,10 @@ class BorderedLabelWidget(QtWidgets.QFrame): if color_value: color = color_value.get_qcolor() - top_left_w = _HTopCornerLineWidget(color, True, self) - top_right_w = _HTopCornerLineWidget(color, False, self) + line_size = 1 + + top_left_w = _HTopCornerLineWidget(color, line_size, True, self) + top_right_w = _HTopCornerLineWidget(color, line_size, False, self) label_widget = QtWidgets.QLabel(label, self) @@ -175,10 +213,10 @@ class BorderedLabelWidget(QtWidgets.QFrame): top_layout.addWidget(label_widget, 0) top_layout.addWidget(top_right_w, 1) - left_w = _VLineWidget(color, True, self) - right_w = _VLineWidget(color, False, self) + left_w = _VLineWidget(color, line_size, True, self) + right_w = _VLineWidget(color, line_size, False, self) - bottom_w = _HBottomLineWidget(color, self) + bottom_w = _HBottomLineWidget(color, line_size, self) center_layout = QtWidgets.QHBoxLayout() center_layout.setContentsMargins(5, 5, 5, 5) @@ -201,6 +239,7 @@ class BorderedLabelWidget(QtWidgets.QFrame): self._widget = None self._radius = 0 + self._line_size = line_size self._top_left_w = top_left_w self._top_right_w = top_right_w @@ -216,14 +255,38 @@ class BorderedLabelWidget(QtWidgets.QFrame): value, value, value, value ) + def set_line_size(self, line_size): + if self._line_size == line_size: + return + self._line_size = line_size + for widget in ( + self._top_left_w, + self._top_right_w, + self._left_w, + self._right_w, + self._bottom_w + ): + widget.set_line_size(line_size) + self._recalculate_sizes() + def showEvent(self, event): super(BorderedLabelWidget, self).showEvent(event) + self._recalculate_sizes() + def _recalculate_sizes(self): height = self._label_widget.height() - radius = (height + (height % 2)) / 2 + radius = int((height + (height % 2)) / 2) self._radius = radius - side_width = 1 + radius + radius_size = self._line_size + 1 + if radius_size < radius: + radius_size = radius + + if radius: + side_width = self._line_size + radius + else: + side_width = self._line_size + 1 + # Don't use fixed width/height as that would set also set # the other size (When fixed width is set then is also set # fixed height). @@ -231,8 +294,8 @@ class BorderedLabelWidget(QtWidgets.QFrame): self._left_w.setMaximumWidth(side_width) self._right_w.setMinimumWidth(side_width) self._right_w.setMaximumWidth(side_width) - self._bottom_w.setMinimumHeight(radius) - self._bottom_w.setMaximumHeight(radius) + self._bottom_w.setMinimumHeight(radius_size) + self._bottom_w.setMaximumHeight(radius_size) self._bottom_w.set_radius(radius) self._top_right_w.set_radius(radius) self._top_left_w.set_radius(radius) From 26b99db61e4ab17033bfaa5b0f9099a3b6561275 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 30 May 2023 15:18:53 +0200 Subject: [PATCH 174/189] removed unused import --- openpype/tools/publisher/widgets/border_label_widget.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/tools/publisher/widgets/border_label_widget.py b/openpype/tools/publisher/widgets/border_label_widget.py index 1381d74eb3..e5693368b1 100644 --- a/openpype/tools/publisher/widgets/border_label_widget.py +++ b/openpype/tools/publisher/widgets/border_label_widget.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- -from math import ceil from qtpy import QtWidgets, QtCore, QtGui from openpype.style import get_objected_colors From 04e6f5f4bb844b11dbb93475f5d023c2125651ff Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 30 May 2023 17:16:18 +0200 Subject: [PATCH 175/189] :bug: fix support for separate AOVs and some style issues --- openpype/hosts/max/api/lib_renderproducts.py | 76 +++++++++---------- .../max/plugins/publish/collect_render.py | 10 ++- 2 files changed, 46 insertions(+), 40 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index a6427bf7c5..81057db733 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -3,22 +3,20 @@ # arnold # https://help.autodesk.com/view/ARNOL/ENU/?guid=arnold_for_3ds_max_ax_maxscript_commands_ax_renderview_commands_html import os + from pymxs import runtime as rt -from openpype.hosts.max.api.lib import ( - get_current_renderer -) -from openpype.settings import get_project_settings + +from openpype.hosts.max.api.lib import get_current_renderer from openpype.pipeline import legacy_io +from openpype.settings import get_project_settings class RenderProducts(object): def __init__(self, project_settings=None): - self._project_settings = project_settings - if not self._project_settings: - self._project_settings = get_project_settings( - legacy_io.Session["AVALON_PROJECT"] - ) + self._project_settings = project_settings or get_project_settings( + legacy_io.Session["AVALON_PROJECT"] + ) def get_beauty(self, container): render_dir = os.path.dirname(rt.rendOutputFilename) @@ -29,14 +27,14 @@ class RenderProducts(object): setting = self._project_settings img_fmt = setting["max"]["RenderSettings"]["image_format"] # noqa - startFrame = int(rt.rendStart) - endFrame = int(rt.rendEnd) + 1 + start_frame = int(rt.rendStart) + end_frame = int(rt.rendEnd) + 1 - render_dict = { + return { "beauty": self.get_expected_beauty( - output_file, startFrame, endFrame, img_fmt) + output_file, start_frame, end_frame, img_fmt + ) } - return render_dict def get_aovs(self, container): render_dir = os.path.dirname(rt.rendOutputFilename) @@ -47,8 +45,8 @@ class RenderProducts(object): setting = self._project_settings img_fmt = setting["max"]["RenderSettings"]["image_format"] # noqa - startFrame = int(rt.rendStart) - endFrame = int(rt.rendEnd) + 1 + start_frame = int(rt.rendStart) + end_frame = int(rt.rendEnd) + 1 renderer_class = get_current_renderer() renderer = str(renderer_class).split(":")[0] render_dict = {} @@ -65,38 +63,40 @@ class RenderProducts(object): for name in render_name: render_dict.update({ name: self.get_expected_render_elements( - output_file, name, startFrame, - endFrame, img_fmt) + output_file, name, start_frame, + end_frame, img_fmt) }) - if renderer == "Redshift_Renderer": + elif renderer == "Redshift_Renderer": render_name = self.get_render_elements_name() if render_name: - rs_AovFiles = rt.RedShift_Renderer().separateAovFiles - if img_fmt == "exr" and not rs_AovFiles: + rs_aov_files = rt.Execute("renderers.current.separateAovFiles") + # this doesn't work, always returns False + # rs_AovFiles = rt.RedShift_Renderer().separateAovFiles + if img_fmt == "exr" and not rs_aov_files: for name in render_name: if name == "RsCryptomatte": render_dict.update({ name: self.get_expected_render_elements( - output_file, name, startFrame, - endFrame, img_fmt) + output_file, name, start_frame, + end_frame, img_fmt) }) else: for name in render_name: render_dict.update({ name: self.get_expected_render_elements( - output_file, name, startFrame, - endFrame, img_fmt) + output_file, name, start_frame, + end_frame, img_fmt) }) - if renderer == "Arnold": + elif renderer == "Arnold": render_name = self.get_arnold_product_name() if render_name: for name in render_name: render_dict.update({ name: self.get_expected_arnold_product( - output_file, name, startFrame, endFrame, img_fmt) + output_file, name, start_frame, end_frame, img_fmt) }) - if renderer in [ + elif renderer in [ "V_Ray_6_Hotfix_3", "V_Ray_GPU_6_Hotfix_3" ]: @@ -106,15 +106,15 @@ class RenderProducts(object): for name in render_name: render_dict.update({ name: self.get_expected_render_elements( - output_file, name, startFrame, - endFrame, img_fmt) # noqa + output_file, name, start_frame, + end_frame, img_fmt) # noqa }) return render_dict - def get_expected_beauty(self, folder, startFrame, endFrame, fmt): + def get_expected_beauty(self, folder, start_frame, end_frame, fmt): beauty_frame_range = [] - for f in range(startFrame, endFrame): + for f in range(start_frame, end_frame): frame = "%04d" % f beauty_output = f"{folder}.{frame}.{fmt}" beauty_output = beauty_output.replace("\\", "/") @@ -134,19 +134,17 @@ class RenderProducts(object): return for i in range(aov_group_num): # get the specific AOV group - for aov in aov_mgr.drivers[i].aov_list: - aov_name.append(aov.name) - + aov_name.extend(aov.name for aov in aov_mgr.drivers[i].aov_list) # close the AOVs manager window amw.close() return aov_name def get_expected_arnold_product(self, folder, name, - startFrame, endFrame, fmt): + start_frame, end_frame, fmt): """Get all the expected Arnold AOVs""" aov_list = [] - for f in range(startFrame, endFrame): + for f in range(start_frame, end_frame): frame = "%04d" % f render_element = f"{folder}_{name}.{frame}.{fmt}" render_element = render_element.replace("\\", "/") @@ -171,10 +169,10 @@ class RenderProducts(object): return render_name def get_expected_render_elements(self, folder, name, - startFrame, endFrame, fmt): + start_frame, end_frame, fmt): """Get all the expected render element output files. """ render_elements = [] - for f in range(startFrame, endFrame): + for f in range(start_frame, end_frame): frame = "%04d" % f render_element = f"{folder}_{name}.{frame}.{fmt}" render_element = render_element.replace("\\", "/") diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 77ab5f654d..db5c84fad9 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -66,7 +66,7 @@ class CollectRender(pyblish.api.InstancePlugin): instance.data["attachTo"] = [] renderer_class = get_current_renderer() renderer = str(renderer_class).split(":")[0] - # also need to get the render dir for coversion + # also need to get the render dir for conversion data = { "asset": asset, "subset": str(instance.name), @@ -84,4 +84,12 @@ class CollectRender(pyblish.api.InstancePlugin): "farm": True } instance.data.update(data) + + # TODO: this should be unified with maya and its "multipart" flag + # on instance. + if renderer == "Redshift_Renderer": + instance.data.update( + {"separateAovFiles": rt.Execute( + "renderers.current.separateAovFiles")}) + self.log.info("data: {0}".format(data)) From be523bc5ec967efce3181668a932eb644a5e8f59 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 30 May 2023 16:49:49 +0100 Subject: [PATCH 176/189] Use temp folder to copy commandlet project --- openpype/hosts/unreal/ue_workers.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/ue_workers.py b/openpype/hosts/unreal/ue_workers.py index e7a690ac9c..2b7e1375e6 100644 --- a/openpype/hosts/unreal/ue_workers.py +++ b/openpype/hosts/unreal/ue_workers.py @@ -6,6 +6,8 @@ import subprocess from distutils import dir_util from pathlib import Path from typing import List, Union +import tempfile +from distutils.dir_util import copy_tree import openpype.hosts.unreal.lib as ue_lib @@ -90,9 +92,20 @@ class UEProjectGenerationWorker(QtCore.QObject): ("Generating a new UE project ... 1 out of " f"{stage_count}")) + # Need to copy the commandlet project to a temporary folder where + # users don't need admin rights to write to. + cmdlet_tmp = tempfile.TemporaryDirectory() + cmdlet_filename = cmdlet_project.name + cmdlet_dir = cmdlet_project.parent.as_posix() + cmdlet_tmp_name = Path(cmdlet_tmp.name) + cmdlet_tmp_file = cmdlet_tmp_name.joinpath(cmdlet_filename) + copy_tree( + cmdlet_dir, + cmdlet_tmp_name.as_posix()) + commandlet_cmd = [ f"{ue_editor_exe.as_posix()}", - f"{cmdlet_project.as_posix()}", + f"{cmdlet_tmp_file.as_posix()}", "-run=AyonGenerateProject", f"{project_file.resolve().as_posix()}", ] @@ -111,6 +124,8 @@ class UEProjectGenerationWorker(QtCore.QObject): gen_process.stdout.close() return_code = gen_process.wait() + cmdlet_tmp.cleanup() + if return_code and return_code != 0: msg = ( f"Failed to generate {self.project_name} " From a215e4f00665f779f1056f5a128b3e2674e3c68f Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 31 May 2023 03:26:47 +0000 Subject: [PATCH 177/189] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index c24388b2ff..5c7105e7e0 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.9-nightly.1" +__version__ = "3.15.9-nightly.2" From e8b47be8d562e8bb1edd77dd4aebf6e75f6fc720 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 31 May 2023 03:27:31 +0000 Subject: [PATCH 178/189] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 54a4ee6ac0..0036e121b7 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.15.9-nightly.2 - 3.15.9-nightly.1 - 3.15.8 - 3.15.8-nightly.3 @@ -134,7 +135,6 @@ body: - 3.14.2 - 3.14.2-nightly.5 - 3.14.2-nightly.4 - - 3.14.2-nightly.3 validations: required: true - type: dropdown From e6d10fa3358b61eae7c9461da2119af72adf9be6 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Wed, 31 May 2023 10:37:21 +0100 Subject: [PATCH 179/189] Update settings_project_global.md (#5045) --- website/docs/project_settings/settings_project_global.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index c17f707830..7bd24a5773 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -63,7 +63,7 @@ Example here describes use case for creation of new color coded review of png im ![global_oiio_transcode](assets/global_oiio_transcode.png) Another use case is to transcode in Maya only `beauty` render layers and use collected `Display` and `View` colorspaces from DCC. -![global_oiio_transcode_in_Maya](assets/global_oiio_transcode.png)n +![global_oiio_transcode_in_Maya](assets/global_oiio_transcode2.png) ## Profile filters From e22d1bf78b4e9d4aeeb7c0295c8bb92b1c5595d9 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 31 May 2023 10:45:47 +0100 Subject: [PATCH 180/189] Set dev mode off by default --- openpype/settings/defaults/project_settings/unreal.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/unreal.json b/openpype/settings/defaults/project_settings/unreal.json index 737a17d289..92bdb468ba 100644 --- a/openpype/settings/defaults/project_settings/unreal.json +++ b/openpype/settings/defaults/project_settings/unreal.json @@ -15,6 +15,6 @@ "preroll_frames": 0, "render_format": "png", "project_setup": { - "dev_mode": true + "dev_mode": false } } From c2b753326fb6cb6460a43e401fb5731cdee1fabb Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 31 May 2023 11:59:21 +0100 Subject: [PATCH 181/189] Check if the Unreal app name follows the right format --- openpype/hosts/unreal/addon.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/openpype/hosts/unreal/addon.py b/openpype/hosts/unreal/addon.py index 1119b5c16c..fddceb00a8 100644 --- a/openpype/hosts/unreal/addon.py +++ b/openpype/hosts/unreal/addon.py @@ -1,5 +1,7 @@ import os +import re from openpype.modules import IHostAddon, OpenPypeModule +from openpype.widgets.message_window import Window UNREAL_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -19,6 +21,18 @@ class UnrealAddon(OpenPypeModule, IHostAddon): from .lib import get_compatible_integration + pattern = re.compile(r'^\d+-\d+$') + + if not pattern.match(app.name): + Window( + parent=None, + title="Unreal application name format", + message="Unreal application name must be in format '5-0' or '5-1'", + level="critical") + raise ValueError( + "Unreal application name must be in format '5-0' or '5-1'" + ) + ue_version = app.name.replace("-", ".") unreal_plugin_path = os.path.join( UNREAL_ROOT_DIR, "integration", "UE_{}".format(ue_version), "Ayon" From 142ae35d9b81b5325c462b67cc870c959a801524 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 31 May 2023 12:13:00 +0100 Subject: [PATCH 182/189] Hound fixes --- openpype/hosts/unreal/addon.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/unreal/addon.py b/openpype/hosts/unreal/addon.py index fddceb00a8..16f6fcf27c 100644 --- a/openpype/hosts/unreal/addon.py +++ b/openpype/hosts/unreal/addon.py @@ -24,14 +24,13 @@ class UnrealAddon(OpenPypeModule, IHostAddon): pattern = re.compile(r'^\d+-\d+$') if not pattern.match(app.name): + msg = "Unreal application name must be in format '5-0' or '5-1'" Window( parent=None, title="Unreal application name format", - message="Unreal application name must be in format '5-0' or '5-1'", + message=msg, level="critical") - raise ValueError( - "Unreal application name must be in format '5-0' or '5-1'" - ) + raise ValueError(msg) ue_version = app.name.replace("-", ".") unreal_plugin_path = os.path.join( From 0635c39a37094bf4f78898a9a179fbf657baf021 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 31 May 2023 14:32:24 +0100 Subject: [PATCH 183/189] Improved error message --- openpype/hosts/unreal/addon.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/addon.py b/openpype/hosts/unreal/addon.py index 16f6fcf27c..ed23950b35 100644 --- a/openpype/hosts/unreal/addon.py +++ b/openpype/hosts/unreal/addon.py @@ -24,7 +24,10 @@ class UnrealAddon(OpenPypeModule, IHostAddon): pattern = re.compile(r'^\d+-\d+$') if not pattern.match(app.name): - msg = "Unreal application name must be in format '5-0' or '5-1'" + msg = ( + "Unreal application key in the settings must be in format" + "'5-0' or '5-1'" + ) Window( parent=None, title="Unreal application name format", From 396486067bd4c33f0ac6ef75f0676e05b9b6c985 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 31 May 2023 13:43:50 +0000 Subject: [PATCH 184/189] [Automated] Release --- CHANGELOG.md | 335 ++++++++++++++++++++++++++++++++++++++++++++ openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 337 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a33904735b..ec6544e659 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,341 @@ # Changelog +## [3.15.9](https://github.com/ynput/OpenPype/tree/3.15.9) + + +[Full Changelog](https://github.com/ynput/OpenPype/compare/3.15.8...3.15.9) + +### **πŸ†• New features** + + +
+Blender: Implemented Loading of Alembic Camera #4990 + +Implemented loading of Alembic cameras in Blender. + + +___ + +
+ + +
+Unreal: Implemented Creator, Loader and Extractor for Levels #5008 + +Creator, Loader and Extractor for Unreal Levels have been implemented. + + +___ + +
+ +### **πŸš€ Enhancements** + + +
+Blender: Added setting for base unit scale #4987 + +A setting for the base unit scale has been added for Blender.The unit scale is automatically applied when opening a file or creating a new one. + + +___ + +
+ + +
+Unreal: Changed naming and path of Camera Levels #5010 + +The levels created for the camera in Unreal now include `_camera` in the name, to be better identifiable, and are placed in the camera folder. + + +___ + +
+ + +
+Settings: Added option to nest settings templates #5022 + +It is possible to nest settings templates in another templates. + + +___ + +
+ + +
+Enhancement/publisher: Remove "hit play to continue" label on continue #5029 + +Remove "hit play to continue" message on continue so that it doesn't show anymore when play was clicked. + + +___ + +
+ + +
+Ftrack: Limit number of ftrack events to query at once #5033 + +Limit the amount of ftrack events received from mongo at once to 100. + + +___ + +
+ + +
+General: Small code cleanups #5034 + +Small code cleanup and updates. + + +___ + +
+ + +
+Global: collect frames to fix with settings #5036 + +Settings for `Collect Frames to Fix` will allow disable per project the plugin. Also `Rewriting latest version` attribute is hiddable from settings. + + +___ + +
+ + +
+General: Publish plugin apply settings can expect only project settings #5037 + +Only project settings are passed to optional `apply_settings` method, if the method expects only one argument. + + +___ + +
+ +### **πŸ› Bug fixes** + + +
+Maya: Load Assembly fix invalid imports #4859 + +Refactors imports so they are now correct. + + +___ + +
+ + +
+Maya: Skipping rendersetup for members. #4973 + +When publishing a `rendersetup`, the objectset is and should be empty. + + +___ + +
+ + +
+Maya: Validate Rig Output IDs #5016 + +Absolute names of node were not used, so plugin did not fetch the nodes properly.Also missed pymel command. + + +___ + +
+ + +
+Deadline: escape rootless path in publish job #4910 + +If the publish path on Deadline job contains spaces or other characters, command was failing because the path wasn't properly escaped. This is fixing it. + + +___ + +
+ + +
+General: Company name and URL changed #4974 + +The current records were obsolete in inno_setup, changed to the up-to-date. +___ + +
+ + +
+Unreal: Fix usage of 'get_full_path' function #5014 + +This PR changes all the occurrences of `get_full_path` functions to alternatives to get the path of the objects. + + +___ + +
+ + +
+Unreal: Fix sequence frames validator to use correct data #5021 + +Fix sequence frames validator to use clipIn and clipOut data instead of frameStart and frameEnd. + + +___ + +
+ + +
+Unreal: Fix render instances collection to use correct data #5023 + +Fix render instances collection to use `frameStart` and `frameEnd` from the Project Manager, instead of the sequence's ones. + + +___ + +
+ + +
+Resolve: loader is opening even if no timeline in project #5025 + +Loader is opening now even no timeline is available in a project. + + +___ + +
+ + +
+nuke: callback for dirmapping is on demand #5030 + +Nuke was slowed down on processing due this callback. Since it is disabled by default it made sense to add it only on demand. + + +___ + +
+ + +
+Publisher: UI works with instances without label #5032 + +Publisher UI does not crash if instance don't have filled 'label' key in instance data. + + +___ + +
+ + +
+Publisher: Call explicitly prepared tab methods #5044 + +It is not possible to go to Create tab during publishing from OpenPype menu. + + +___ + +
+ + +
+Ftrack: Role names are not case sensitive in ftrack event server status action #5058 + +Event server status action is not case sensitive for role names of user. + + +___ + +
+ + +
+Publisher: Fix border widget #5063 + +Fixed border lines in Publisher UI to be painted correctly with correct indentation and size. + + +___ + +
+ + +
+Unreal: Fix Commandlet Project and Permissions #5066 + +Fix problem when creating an Unreal Project when Commandlet Project is in a protected location. + + +___ + +
+ + +
+Unreal: Added verification for Unreal app name format #5070 + +The Unreal app name is used to determine the Unreal version folder, so it is necessary that if follows the format `x-x`, where `x` is any integer. This PR adds a verification that the app name follows that format. + + +___ + +
+ +### **πŸ“ƒ Documentation** + + +
+Docs: Display wrong image in ExtractOIIOTranscode #5045 + +Wrong image display in `https://openpype.io/docs/project_settings/settings_project_global#extract-oiio-transcode`. + + +___ + +
+ +### **Merged pull requests** + + +
+Drop-down menu to list all families in create placeholder #4928 + +Currently in the create placeholder window, we need to write the family manually. This replace the text field by an enum field with all families for the current software. + + +___ + +
+ + +
+add sync to specific projects or listen only #4919 + +Extend kitsu sync service with additional arguments to sync specific projects. + + +___ + +
+ + + + ## [3.15.8](https://github.com/ynput/OpenPype/tree/3.15.8) diff --git a/openpype/version.py b/openpype/version.py index 5c7105e7e0..dd23138dee 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.9-nightly.2" +__version__ = "3.15.9" diff --git a/pyproject.toml b/pyproject.toml index a72a3d66d7..633899d3a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.15.8" # OpenPype +version = "3.15.9" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 776dcb5a6fcf65eec08f95655c870d7e43ec4ce9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 31 May 2023 13:44:50 +0000 Subject: [PATCH 185/189] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 0036e121b7..aa5b8decdc 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.15.9 - 3.15.9-nightly.2 - 3.15.9-nightly.1 - 3.15.8 @@ -134,7 +135,6 @@ body: - 3.14.3-nightly.1 - 3.14.2 - 3.14.2-nightly.5 - - 3.14.2-nightly.4 validations: required: true - type: dropdown From 3955c466c3a0f01e20bf95bfa594b561884517e5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 31 May 2023 17:17:35 +0200 Subject: [PATCH 186/189] fix apply settings on hiero loader (#5073) --- openpype/hosts/hiero/plugins/load/load_clip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/hiero/plugins/load/load_clip.py b/openpype/hosts/hiero/plugins/load/load_clip.py index 77844d2448..c9bebfa8b2 100644 --- a/openpype/hosts/hiero/plugins/load/load_clip.py +++ b/openpype/hosts/hiero/plugins/load/load_clip.py @@ -41,8 +41,8 @@ class LoadClip(phiero.SequenceLoader): clip_name_template = "{asset}_{subset}_{representation}" + @classmethod def apply_settings(cls, project_settings, system_settings): - plugin_type_settings = ( project_settings .get("hiero", {}) From aeaad018c1af7b30c9d1377d27ed91bec65e91cd Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 31 May 2023 18:12:48 +0200 Subject: [PATCH 187/189] :rotating_light: make peace with the hound :dog: --- openpype/hosts/max/api/lib_renderproducts.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 81057db733..94b0aeb913 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -15,14 +15,12 @@ class RenderProducts(object): def __init__(self, project_settings=None): self._project_settings = project_settings or get_project_settings( - legacy_io.Session["AVALON_PROJECT"] - ) + legacy_io.Session["AVALON_PROJECT"]) def get_beauty(self, container): render_dir = os.path.dirname(rt.rendOutputFilename) - output_file = os.path.join(render_dir, - container) + output_file = os.path.join(render_dir, container) setting = self._project_settings img_fmt = setting["max"]["RenderSettings"]["image_format"] # noqa From 5379679f869db99bf169263ca43bb6f068df9dbf Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 1 Jun 2023 07:23:26 +0000 Subject: [PATCH 188/189] update README.md [skip ci] --- README.md | 75 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 39 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 514ffb62c0..8757e3db92 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -[![All Contributors](https://img.shields.io/badge/all_contributors-27-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-28-orange.svg?style=flat-square)](#contributors-) OpenPype ==== @@ -303,41 +303,44 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Milan Kolar

πŸ’» πŸ“– πŸš‡ πŸ’Ό πŸ–‹ πŸ” 🚧 πŸ“† πŸ‘€ πŸ§‘β€πŸ« πŸ’¬

Jakub JeΕΎek

πŸ’» πŸ“– πŸš‡ πŸ–‹ πŸ‘€ 🚧 πŸ§‘β€πŸ« πŸ“† πŸ’¬

OndΕ™ej Samohel

πŸ’» πŸ“– πŸš‡ πŸ–‹ πŸ‘€ 🚧 πŸ§‘β€πŸ« πŸ“† πŸ’¬

Jakub Trllo

πŸ’» πŸ“– πŸš‡ πŸ‘€ 🚧 πŸ’¬

Petr Kalis

πŸ’» πŸ“– πŸš‡ πŸ‘€ 🚧 πŸ’¬

64qam

πŸ’» πŸ‘€ πŸ“– πŸš‡ πŸ“† 🚧 πŸ–‹ πŸ““

Roy Nieterau

πŸ’» πŸ“– πŸ‘€ πŸ§‘β€πŸ« πŸ’¬

Toke Jepsen

πŸ’» πŸ“– πŸ‘€ πŸ§‘β€πŸ« πŸ’¬

Jiri Sindelar

πŸ’» πŸ‘€ πŸ“– πŸ–‹ βœ… πŸ““

Simone Barbieri

πŸ’» πŸ“–

karimmozilla

πŸ’»

Allan I. A.

πŸ’»

murphy

πŸ’» πŸ‘€ πŸ““ πŸ“– πŸ“†

Wijnand Koreman

πŸ’»

Bo Zhou

πŸ’»

ClΓ©ment Hector

πŸ’» πŸ‘€

David Lai

πŸ’» πŸ‘€

Derek

πŸ’» πŸ“–

GΓ‘bor Marinov

πŸ’» πŸ“–

icyvapor

πŸ’» πŸ“–

JΓ©rΓ΄me LORRAIN

πŸ’»

David Morris-Oliveros

πŸ’»

BenoitConnan

πŸ’»

Malthaldar

πŸ’»

Sven Neve

πŸ’»

zafrs

πŸ’»

FΓ©lix David

πŸ’» πŸ“–
Milan Kolar
Milan Kolar

πŸ’» πŸ“– πŸš‡ πŸ’Ό πŸ–‹ πŸ” 🚧 πŸ“† πŸ‘€ πŸ§‘β€πŸ« πŸ’¬
Jakub JeΕΎek
Jakub JeΕΎek

πŸ’» πŸ“– πŸš‡ πŸ–‹ πŸ‘€ 🚧 πŸ§‘β€πŸ« πŸ“† πŸ’¬
OndΕ™ej Samohel
OndΕ™ej Samohel

πŸ’» πŸ“– πŸš‡ πŸ–‹ πŸ‘€ 🚧 πŸ§‘β€πŸ« πŸ“† πŸ’¬
Jakub Trllo
Jakub Trllo

πŸ’» πŸ“– πŸš‡ πŸ‘€ 🚧 πŸ’¬
Petr Kalis
Petr Kalis

πŸ’» πŸ“– πŸš‡ πŸ‘€ 🚧 πŸ’¬
64qam
64qam

πŸ’» πŸ‘€ πŸ“– πŸš‡ πŸ“† 🚧 πŸ–‹ πŸ““
Roy Nieterau
Roy Nieterau

πŸ’» πŸ“– πŸ‘€ πŸ§‘β€πŸ« πŸ’¬
Toke Jepsen
Toke Jepsen

πŸ’» πŸ“– πŸ‘€ πŸ§‘β€πŸ« πŸ’¬
Jiri Sindelar
Jiri Sindelar

πŸ’» πŸ‘€ πŸ“– πŸ–‹ βœ… πŸ““
Simone Barbieri
Simone Barbieri

πŸ’» πŸ“–
karimmozilla
karimmozilla

πŸ’»
Allan I. A.
Allan I. A.

πŸ’»
murphy
murphy

πŸ’» πŸ‘€ πŸ““ πŸ“– πŸ“†
Wijnand Koreman
Wijnand Koreman

πŸ’»
Bo Zhou
Bo Zhou

πŸ’»
ClΓ©ment Hector
ClΓ©ment Hector

πŸ’» πŸ‘€
David Lai
David Lai

πŸ’» πŸ‘€
Derek
Derek

πŸ’» πŸ“–
GΓ‘bor Marinov
GΓ‘bor Marinov

πŸ’» πŸ“–
icyvapor
icyvapor

πŸ’» πŸ“–
JΓ©rΓ΄me LORRAIN
JΓ©rΓ΄me LORRAIN

πŸ’»
David Morris-Oliveros
David Morris-Oliveros

πŸ’»
BenoitConnan
BenoitConnan

πŸ’»
Malthaldar
Malthaldar

πŸ’»
Sven Neve
Sven Neve

πŸ’»
zafrs
zafrs

πŸ’»
FΓ©lix David
FΓ©lix David

πŸ’» πŸ“–
Alexey Bogomolov
Alexey Bogomolov

πŸ’»
From 28e9da8918b86d6ef30f2ffb549483edef3e7a24 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 1 Jun 2023 07:23:27 +0000 Subject: [PATCH 189/189] update .all-contributorsrc [skip ci] --- .all-contributorsrc | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index b30f3b2499..60812cdb3c 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1,6 +1,6 @@ { "projectName": "OpenPype", - "projectOwner": "pypeclub", + "projectOwner": "ynput", "repoType": "github", "repoHost": "https://github.com", "files": [ @@ -319,8 +319,18 @@ "code", "doc" ] + }, + { + "login": "movalex", + "name": "Alexey Bogomolov", + "avatar_url": "https://avatars.githubusercontent.com/u/11698866?v=4", + "profile": "http://abogomolov.com", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, - "skipCi": true + "skipCi": true, + "commitType": "docs" }