From b0055516e6631d26fedfd499769984a7f2963f79 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 29 Mar 2022 15:21:35 +0200 Subject: [PATCH 001/228] OP-2834 - Fix getting non-active model panel. --- .../maya/plugins/publish/extract_playblast.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index bb1ecf279d..7fa9a840e9 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -90,6 +90,24 @@ class ExtractPlayblast(openpype.api.Extractor): else: preset["viewport_options"] = {"imagePlane": image_plane} + # Image planes do not update the file sequence unless the active panel + # is viewing through the camera. + model_panel = instance.context.data.get("model_panel") + if not model_panel: + model_panels = cmds.getPanel(type="modelPanel") + visible_panels = cmds.getPanel(visiblePanels=True) + model_panel = list( + set(visible_panels) - (set(visible_panels) - set(model_panels)) + )[0] + instance.context.data["model_panel"] = model_panel + + panel_camera = instance.context.data.get("panel_camera") + if not panel_camera: + panel_camera = capture.parse_view(model_panel)["camera"] + instance.context.data["panel_camera"] = panel_camera + + cmds.modelPanel(model_panel, edit=True, camera=preset["camera"]) + with maintained_time(): filename = preset.get("filename", "%TEMP%") @@ -108,6 +126,9 @@ class ExtractPlayblast(openpype.api.Extractor): path = capture.capture(**preset) + # Restore panel camera. + cmds.modelPanel(model_panel, edit=True, camera=panel_camera) + self.log.debug("playblast path {}".format(path)) collected_files = os.listdir(stagingdir) From 3770eb69b653f31292faee388ed416f7bf8a171f Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 29 Mar 2022 18:34:34 +0300 Subject: [PATCH 002/228] add rstex function --- .../maya/plugins/publish/extract_look.py | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index a8893072d0..1516495278 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -6,6 +6,7 @@ import json import tempfile import contextlib import subprocess +from openpype.lib.vendor_bin_utils import find_executable from collections import OrderedDict from maya import cmds # noqa @@ -42,6 +43,58 @@ def find_paths_by_hash(texture_hash): return io.distinct(key, {"type": "version"}) +def rstex(source, *args): + """Make `.rstexbin` using `redshiftTextureProcessor` + with some default settings. + + This function requires the `REDSHIFT_COREDATAPATH` + to be in `PATH`. + + Args: + source (str): Path to source file. + *args: Additional arguments for `redshiftTextureProcessor`. + + Returns: + str: Output of `redshiftTextureProcessor` command. + + """ + if "REDSHIFT_COREDATAPATH" not in os.environ: + raise RuntimeError("Must have Redshift available.") + + redshift_bin_path = os.path.join( + os.environ["REDSHIFT_COREDATAPATH"], + "bin", + "redshiftTextureProcessor" + ) + + texture_processor_path = find_executable(redshift_bin_path) + + cmd = [ + texture_processor_path, + escape_space(source), + + ] + + cmd.extend(args) + + cmd = " ".join(cmd) + + CREATE_NO_WINDOW = 0x08000000 + kwargs = dict(args=cmd, stderr=subprocess.STDOUT) + + if sys.platform == "win32": + kwargs["creationflags"] = CREATE_NO_WINDOW + try: + out = subprocess.check_output(**kwargs) + except subprocess.CalledProcessError as exc: + print(exc) + import traceback + + traceback.print_exc() + raise + return out + + def maketx(source, destination, *args): """Make `.tx` using `maketx` with some default settings. From 72b45229e943c419083e243ae27a540373edd6e2 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 29 Mar 2022 19:23:25 +0300 Subject: [PATCH 003/228] fix style warnings --- openpype/hosts/maya/plugins/publish/extract_look.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 1516495278..6102b311a3 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -65,10 +65,10 @@ def rstex(source, *args): os.environ["REDSHIFT_COREDATAPATH"], "bin", "redshiftTextureProcessor" - ) + ) texture_processor_path = find_executable(redshift_bin_path) - + cmd = [ texture_processor_path, escape_space(source), @@ -83,7 +83,7 @@ def rstex(source, *args): kwargs = dict(args=cmd, stderr=subprocess.STDOUT) if sys.platform == "win32": - kwargs["creationflags"] = CREATE_NO_WINDOW + kwargs["creationflags"] = CREATE_NO_WINDOW try: out = subprocess.check_output(**kwargs) except subprocess.CalledProcessError as exc: From e65a1ea7d144abdcd0681641b45c50d11e903405 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 29 Mar 2022 19:45:00 +0300 Subject: [PATCH 004/228] remove extra line --- openpype/hosts/maya/plugins/publish/extract_look.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 6102b311a3..cd647a6733 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -71,8 +71,7 @@ def rstex(source, *args): cmd = [ texture_processor_path, - escape_space(source), - + escape_space(source), ] cmd.extend(args) From 2406f78f4717451c2408a21b0f36bd66bd4ec95b Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 29 Mar 2022 19:45:49 +0300 Subject: [PATCH 005/228] fix trailing whitespace --- openpype/hosts/maya/plugins/publish/extract_look.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index cd647a6733..fb90d7538b 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -71,7 +71,7 @@ def rstex(source, *args): cmd = [ texture_processor_path, - escape_space(source), + escape_space(source), ] cmd.extend(args) From be840d3a8b8b786c3fdfb56ab8ef7577c04747f3 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 4 Apr 2022 15:50:33 +0300 Subject: [PATCH 006/228] add exectuable path finder function --- openpype/lib/vendor_bin_utils.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/openpype/lib/vendor_bin_utils.py b/openpype/lib/vendor_bin_utils.py index 23e28ea304..c30a1ee709 100644 --- a/openpype/lib/vendor_bin_utils.py +++ b/openpype/lib/vendor_bin_utils.py @@ -120,6 +120,26 @@ def get_oiio_tools_path(tool="oiiotool"): return find_executable(os.path.join(oiio_dir, tool)) +def get_redshift_tool(tool_name): + """Path to redshift texture processor. + + On Windows it adds .exe extension if missing from tool argument. + + Args: + tool (string): Tool name. + + Returns: + str: Full path to redshift texture processor executable. + """ + redshift_tool_path = os.path.join( + os.environ["REDSHIFT_COREDATAPATH"], + "bin", + tool_name + ) + + return find_executable(redshift_tool_path) + + def get_ffmpeg_tool_path(tool="ffmpeg"): """Path to vendorized FFmpeg executable. From d4b8d47b18ed1605cdffa9a635586b40e29a6adf Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 4 Apr 2022 15:51:31 +0300 Subject: [PATCH 007/228] use redshift tool finder in extractor --- openpype/hosts/maya/plugins/publish/extract_look.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index fb90d7538b..6ce3a981f4 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -6,7 +6,7 @@ import json import tempfile import contextlib import subprocess -from openpype.lib.vendor_bin_utils import find_executable +from openpype.lib.vendor_bin_utils import get_redshift_tool from collections import OrderedDict from maya import cmds # noqa @@ -61,13 +61,7 @@ def rstex(source, *args): if "REDSHIFT_COREDATAPATH" not in os.environ: raise RuntimeError("Must have Redshift available.") - redshift_bin_path = os.path.join( - os.environ["REDSHIFT_COREDATAPATH"], - "bin", - "redshiftTextureProcessor" - ) - - texture_processor_path = find_executable(redshift_bin_path) + texture_processor_path = get_redshift_tool("TextureProcessor") cmd = [ texture_processor_path, From abc299e86eea14ec425c07e46c5af51551cf1a3e Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 5 Apr 2022 20:00:54 +0300 Subject: [PATCH 008/228] Add redshift texture processing option to schema --- .../schemas/projects_schema/schemas/schema_maya_create.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json index 0544b4bab7..b0bd46d20f 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json @@ -21,6 +21,11 @@ "key": "make_tx", "label": "Make tx files" }, + { + "type": "boolean", + "key": "rstex", + "label": "Make Redshift texture files" + }, { "type": "list", "key": "defaults", From 640415a0959bbc67f0e516b2363c4998b5d74508 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 5 Apr 2022 20:18:42 +0300 Subject: [PATCH 009/228] adjust key name --- .../schemas/projects_schema/schemas/schema_maya_create.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json index b0bd46d20f..bf3c9b3fe8 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json @@ -23,7 +23,7 @@ }, { "type": "boolean", - "key": "rstex", + "key": "rs_tex", "label": "Make Redshift texture files" }, { From 0bcf353c82df569dc4bab517f3f2713f05f29b71 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 5 Apr 2022 20:26:47 +0300 Subject: [PATCH 010/228] add redshift texture create option to look creator --- openpype/hosts/maya/plugins/create/create_look.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/create/create_look.py b/openpype/hosts/maya/plugins/create/create_look.py index 56e2640919..c190a73ade 100644 --- a/openpype/hosts/maya/plugins/create/create_look.py +++ b/openpype/hosts/maya/plugins/create/create_look.py @@ -12,6 +12,7 @@ class CreateLook(plugin.Creator): family = "look" icon = "paint-brush" make_tx = True + rs_tex = False def __init__(self, *args, **kwargs): super(CreateLook, self).__init__(*args, **kwargs) @@ -20,6 +21,7 @@ class CreateLook(plugin.Creator): # Whether to automatically convert the textures to .tx upon publish. self.data["maketx"] = self.make_tx - + # Whether to automatically convert the textures to .rstex upon publish. + self.data["rstex"] = self.rs_tex # Enable users to force a copy. self.data["forceCopy"] = False From 7bbf381c6e8fbd6a12a9ecf58d891f15d0e5ad83 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 5 Apr 2022 20:29:54 +0300 Subject: [PATCH 011/228] add rs_tex option to schema defaults --- openpype/settings/defaults/project_settings/maya.json | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 19d9a95595..7c09fa7891 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -34,6 +34,7 @@ "CreateLook": { "enabled": true, "make_tx": true, + "rs_tex": false, "defaults": [ "Main" ] From 078775e9b6bb5e814d1f284202419760853e631a Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 5 Apr 2022 21:16:02 +0300 Subject: [PATCH 012/228] add rstex variable to process_resources --- openpype/hosts/maya/plugins/publish/extract_look.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 6ce3a981f4..11f62ab80b 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -363,7 +363,8 @@ class ExtractLook(openpype.api.Extractor): # be the input file to multiple nodes. resources = instance.data["resources"] do_maketx = instance.data.get("maketx", False) - + # Option to convert textures to native redshift textures + do_rstex = instance.data.get("rstex", False) # Collect all unique files used in the resources files_metadata = {} for resource in resources: From 6ac5cd4a4a5efad533512b9deea45fe0bba4532a Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Fri, 8 Apr 2022 07:22:49 +0300 Subject: [PATCH 013/228] Add redshift texture processing flag --- openpype/hosts/maya/plugins/publish/extract_look.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 11f62ab80b..87e6625653 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -396,6 +396,7 @@ class ExtractLook(openpype.api.Extractor): source, mode, texture_hash = self._process_texture( filepath, + do_rstex, do_maketx, staging=staging_dir, linearize=linearize, @@ -487,7 +488,7 @@ class ExtractLook(openpype.api.Extractor): resources_dir, basename + ext ) - def _process_texture(self, filepath, do_maketx, staging, linearize, force): + def _process_texture(self, filepath, do_rstex, do_maketx, staging, linearize, force): """Process a single texture file on disk for publishing. This will: 1. Check whether it's already published, if so it will do hardlink From c43ec04003c7763d82d62944cc36c62512f11fb4 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Fri, 8 Apr 2022 07:50:49 +0300 Subject: [PATCH 014/228] add redshift processor call to generate .rstexbin --- openpype/hosts/maya/plugins/publish/extract_look.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 87e6625653..6ce8ff6052 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -548,6 +548,10 @@ class ExtractLook(openpype.api.Extractor): return converted, COPY, texture_hash + self.log.info("Generating .rstexbin file for %s .." % filepath) + # Generates Redshift optimized textures using Redshift processor + if do_rstex: + rstex(filepath) return filepath, COPY, texture_hash From 92c1ac7342d94889abb79a71a9be8b8750d7fba7 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 19 Apr 2022 21:33:11 +0300 Subject: [PATCH 015/228] refactor convertor function to abstract class and inherited class --- .../maya/plugins/publish/extract_look.py | 75 +++++++++++-------- 1 file changed, 43 insertions(+), 32 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index e0a5bff56c..7f23663721 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- """Maya look extractor.""" +from abc import ABC, abstractmethod import os import sys import json @@ -43,50 +44,60 @@ def find_paths_by_hash(texture_hash): key = "data.sourceHashes.{0}".format(texture_hash) return io.distinct(key, {"type": "version"}) +class TextureProcessor(metaclass=ABC.ABCMeta): + def __init__(self): + #TODO: Figure out design for predetermined objects to be initialized. + + @abstractmethod + def process(self, filepath): -def rstex(source, *args): - """Make `.rstexbin` using `redshiftTextureProcessor` - with some default settings. + - This function requires the `REDSHIFT_COREDATAPATH` - to be in `PATH`. +class MakeRSTexBin(TextureProcessor): + + def process(source, *args): + """Make `.rstexbin` using `redshiftTextureProcessor` + with some default settings. - Args: - source (str): Path to source file. - *args: Additional arguments for `redshiftTextureProcessor`. + This function requires the `REDSHIFT_COREDATAPATH` + to be in `PATH`. - Returns: - str: Output of `redshiftTextureProcessor` command. + Args: + source (str): Path to source file. + *args: Additional arguments for `redshiftTextureProcessor`. - """ - if "REDSHIFT_COREDATAPATH" not in os.environ: - raise RuntimeError("Must have Redshift available.") + Returns: + str: Output of `redshiftTextureProcessor` command. - texture_processor_path = get_redshift_tool("TextureProcessor") + """ + if "REDSHIFT_COREDATAPATH" not in os.environ: + raise RuntimeError("Must have Redshift available.") - cmd = [ - texture_processor_path, - escape_space(source), - ] + texture_processor_path = get_redshift_tool("TextureProcessor") - cmd.extend(args) + cmd = [ + texture_processor_path, + escape_space(source), + ] - cmd = " ".join(cmd) + cmd.extend(args) - CREATE_NO_WINDOW = 0x08000000 - kwargs = dict(args=cmd, stderr=subprocess.STDOUT) + cmd = " ".join(cmd) - if sys.platform == "win32": - kwargs["creationflags"] = CREATE_NO_WINDOW - try: - out = subprocess.check_output(**kwargs) - except subprocess.CalledProcessError as exc: - print(exc) - import traceback + CREATE_NO_WINDOW = 0x08000000 + kwargs = dict(args=cmd, stderr=subprocess.STDOUT) - traceback.print_exc() - raise - return out + if sys.platform == "win32": + kwargs["creationflags"] = CREATE_NO_WINDOW + try: + processed_filepath = subprocess.check_output(**kwargs) + except subprocess.CalledProcessError as exc: + print(exc) + import traceback + + traceback.print_exc() + raise + return processed_filepath def maketx(source, destination, *args): From 74f2c78415b091cdbc9bd447549d20dc0b796235 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 19 Apr 2022 21:34:26 +0300 Subject: [PATCH 016/228] refactor tx conversion into class --- .../maya/plugins/publish/extract_look.py | 92 +++++++++---------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 7f23663721..54ab7b7877 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -99,65 +99,65 @@ class MakeRSTexBin(TextureProcessor): raise return processed_filepath +class MakeTX(TextureProcessor): + def process(source, destination, *args): + """Make `.tx` using `maketx` with some default settings. -def maketx(source, destination, *args): - """Make `.tx` using `maketx` with some default settings. + The settings are based on default as used in Arnold's + txManager in the scene. + This function requires the `maketx` executable to be + on the `PATH`. - The settings are based on default as used in Arnold's - txManager in the scene. - This function requires the `maketx` executable to be - on the `PATH`. + Args: + source (str): Path to source file. + destination (str): Writing destination path. + *args: Additional arguments for `maketx`. - Args: - source (str): Path to source file. - destination (str): Writing destination path. - *args: Additional arguments for `maketx`. + Returns: + str: Output of `maketx` command. - Returns: - str: Output of `maketx` command. + """ + from openpype.lib import get_oiio_tools_path - """ - from openpype.lib import get_oiio_tools_path + maketx_path = get_oiio_tools_path("maketx") - maketx_path = get_oiio_tools_path("maketx") + if not os.path.exists(maketx_path): + print( + "OIIO tool not found in {}".format(maketx_path)) + raise AssertionError("OIIO tool not found") - if not os.path.exists(maketx_path): - print( - "OIIO tool not found in {}".format(maketx_path)) - raise AssertionError("OIIO tool not found") + cmd = [ + maketx_path, + "-v", # verbose + "-u", # update mode + # unpremultiply before conversion (recommended when alpha present) + "--unpremult", + "--checknan", + # use oiio-optimized settings for tile-size, planarconfig, metadata + "--oiio", + "--filter lanczos3", + ] - cmd = [ - maketx_path, - "-v", # verbose - "-u", # update mode - # unpremultiply before conversion (recommended when alpha present) - "--unpremult", - "--checknan", - # use oiio-optimized settings for tile-size, planarconfig, metadata - "--oiio", - "--filter lanczos3", - ] + cmd.extend(args) + cmd.extend(["-o", escape_space(destination), escape_space(source)]) - cmd.extend(args) - cmd.extend(["-o", escape_space(destination), escape_space(source)]) + cmd = " ".join(cmd) - cmd = " ".join(cmd) + CREATE_NO_WINDOW = 0x08000000 # noqa + kwargs = dict(args=cmd, stderr=subprocess.STDOUT) - CREATE_NO_WINDOW = 0x08000000 # noqa - kwargs = dict(args=cmd, stderr=subprocess.STDOUT) + if sys.platform == "win32": + kwargs["creationflags"] = CREATE_NO_WINDOW + try: + processed_filepath = subprocess.check_output(**kwargs) + except subprocess.CalledProcessError as exc: + print(exc) + import traceback - if sys.platform == "win32": - kwargs["creationflags"] = CREATE_NO_WINDOW - try: - out = subprocess.check_output(**kwargs) - except subprocess.CalledProcessError as exc: - print(exc) - import traceback + traceback.print_exc() + raise - traceback.print_exc() - raise - - return out + return processed_filepath @contextlib.contextmanager From 7b1346e300c047654d6378c300916a4ac76acf56 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 19 Apr 2022 21:41:48 +0300 Subject: [PATCH 017/228] add processor list and adjust logic for more options later --- openpype/hosts/maya/plugins/publish/extract_look.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 54ab7b7877..4b77a47729 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -374,9 +374,15 @@ class ExtractLook(openpype.api.Extractor): # might be included more than once amongst the resources as they could # be the input file to multiple nodes. resources = instance.data["resources"] + # Specify texture processing executables to activate + processors = [] do_maketx = instance.data.get("maketx", False) + if do_maketx: + processors.append(MakeTX) # Option to convert textures to native redshift textures do_rstex = instance.data.get("rstex", False) + if do_rstex: + processors.append(MakeRSTexBin) # Collect all unique files used in the resources files_metadata = {} for resource in resources: From 0100ea5bf8496915875ad0f7ca90ec1bd93e88de Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 7 Jun 2022 15:46:30 +0300 Subject: [PATCH 018/228] Move redshift tool finder function to extractor. --- .../maya/plugins/publish/extract_look.py | 29 +++++++++++++++---- openpype/lib/vendor_bin_utils.py | 20 ------------- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 92d8f5ab17..357f7a4430 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -8,7 +8,7 @@ import tempfile import platform import contextlib import subprocess -from openpype.lib.vendor_bin_utils import get_redshift_tool +from openpype.lib.vendor_bin_utils import find_executable from collections import OrderedDict from maya import cmds # noqa @@ -46,15 +46,13 @@ def find_paths_by_hash(texture_hash): class TextureProcessor(metaclass=ABC.ABCMeta): def __init__(self): - #TODO: Figure out design for predetermined objects to be initialized. - + #TODO: Figure out design for predetermined objects to be initialized. + @abstractmethod def process(self, filepath): - class MakeRSTexBin(TextureProcessor): - def process(source, *args): """Make `.rstexbin` using `redshiftTextureProcessor` with some default settings. @@ -99,6 +97,7 @@ class MakeRSTexBin(TextureProcessor): raise return processed_filepath + class MakeTX(TextureProcessor): def process(source, destination, *args): """Make `.tx` using `maketx` with some default settings. @@ -603,3 +602,23 @@ class ExtractModelRenderSets(ExtractLook): self.scene_type = self.scene_type_prefix + self.scene_type return typ + + +def get_redshift_tool(tool_name): + """Path to redshift texture processor. + + On Windows it adds .exe extension if missing from tool argument. + + Args: + tool (string): Tool name. + + Returns: + str: Full path to redshift texture processor executable. + """ + redshift_tool_path = os.path.join( + os.environ["REDSHIFT_COREDATAPATH"], + "bin", + tool_name + ) + + return find_executable(redshift_tool_path) diff --git a/openpype/lib/vendor_bin_utils.py b/openpype/lib/vendor_bin_utils.py index cdc1290400..e5ab2872a0 100644 --- a/openpype/lib/vendor_bin_utils.py +++ b/openpype/lib/vendor_bin_utils.py @@ -123,26 +123,6 @@ def get_oiio_tools_path(tool="oiiotool"): return find_executable(os.path.join(oiio_dir, tool)) -def get_redshift_tool(tool_name): - """Path to redshift texture processor. - - On Windows it adds .exe extension if missing from tool argument. - - Args: - tool (string): Tool name. - - Returns: - str: Full path to redshift texture processor executable. - """ - redshift_tool_path = os.path.join( - os.environ["REDSHIFT_COREDATAPATH"], - "bin", - tool_name - ) - - return find_executable(redshift_tool_path) - - def get_ffmpeg_tool_path(tool="ffmpeg"): """Path to vendorized FFmpeg executable. From 2102e4f4fa86778542d96e9758dbb12967b6b762 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 7 Jun 2022 15:46:50 +0300 Subject: [PATCH 019/228] Add variable for redshift os path. --- openpype/hosts/maya/plugins/publish/extract_look.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 357f7a4430..69d7eb78af 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -615,8 +615,10 @@ def get_redshift_tool(tool_name): Returns: str: Full path to redshift texture processor executable. """ + redshift_os_path = os.environ["REDSHIFT_COREDATAPATH"] + redshift_tool_path = os.path.join( - os.environ["REDSHIFT_COREDATAPATH"], + redshift_os_path, "bin", tool_name ) From 406ac826e018ffcac07aa9145f5c532b86ddfd27 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 7 Jun 2022 16:09:53 +0300 Subject: [PATCH 020/228] Style fix --- openpype/hosts/maya/plugins/publish/extract_look.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 69d7eb78af..2e83af1437 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -46,7 +46,7 @@ def find_paths_by_hash(texture_hash): class TextureProcessor(metaclass=ABC.ABCMeta): def __init__(self): - #TODO: Figure out design for predetermined objects to be initialized. + # TODO: Figure out design for predetermined objects to be initialized. @abstractmethod def process(self, filepath): From 710ed3a889ee3d7043501d514d0cc3be3e8d571e Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Wed, 8 Jun 2022 10:37:46 +0300 Subject: [PATCH 021/228] Start moving processors logic. --- .../maya/plugins/publish/extract_look.py | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 2e83af1437..cce2643dba 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -44,6 +44,7 @@ def find_paths_by_hash(texture_hash): key = "data.sourceHashes.{0}".format(texture_hash) return legacy_io.distinct(key, {"type": "version"}) + class TextureProcessor(metaclass=ABC.ABCMeta): def __init__(self): # TODO: Figure out design for predetermined objects to be initialized. @@ -51,6 +52,8 @@ class TextureProcessor(metaclass=ABC.ABCMeta): @abstractmethod def process(self, filepath): + return processed_texture_path + class MakeRSTexBin(TextureProcessor): def process(source, *args): @@ -373,15 +376,6 @@ class ExtractLook(openpype.api.Extractor): # might be included more than once amongst the resources as they could # be the input file to multiple nodes. resources = instance.data["resources"] - # Specify texture processing executables to activate - processors = [] - do_maketx = instance.data.get("maketx", False) - if do_maketx: - processors.append(MakeTX) - # Option to convert textures to native redshift textures - do_rstex = instance.data.get("rstex", False) - if do_rstex: - processors.append(MakeRSTexBin) # Collect all unique files used in the resources files_metadata = {} for resource in resources: @@ -420,8 +414,7 @@ class ExtractLook(openpype.api.Extractor): source, mode, texture_hash = self._process_texture( filepath, - do_rstex, - do_maketx, + processors, staging=staging_dir, linearize=linearize, force=force_copy @@ -514,7 +507,7 @@ class ExtractLook(openpype.api.Extractor): resources_dir, basename + ext ) - def _process_texture(self, filepath, do_rstex, do_maketx, staging, linearize, force): + def _process_texture(self, filepath, processors, staging, linearize, force): """Process a single texture file on disk for publishing. This will: 1. Check whether it's already published, if so it will do hardlink @@ -546,7 +539,17 @@ class ExtractLook(openpype.api.Extractor): ("Paths not found on disk, " "skipping hardlink: %s") % (existing,) ) - + texture_files = self.collect_text + # Specify texture processing executables to activate + processors = [] + do_maketx = instance.data.get("maketx", False) + if do_maketx: + processors.append(MakeTX) + # Option to convert textures to native redshift textures + do_rstex = instance.data.get("rstex", False) + if do_rstex: + processors.append(MakeRSTexBin) + if do_maketx and ext != ".tx": # Produce .tx file in staging if source file is not .tx converted = os.path.join(staging, "resources", fname + ".tx") From 68caaa7528882654077a208b835a712f3e319850 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Thu, 9 Jun 2022 10:56:26 +0300 Subject: [PATCH 022/228] move function --- .../maya/plugins/publish/extract_look.py | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index cce2643dba..d6c3588280 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -24,6 +24,28 @@ COPY = 1 HARDLINK = 2 +def get_redshift_tool(tool_name): + """Path to redshift texture processor. + + On Windows it adds .exe extension if missing from tool argument. + + Args: + tool (string): Tool name. + + Returns: + str: Full path to redshift texture processor executable. + """ + redshift_os_path = os.environ["REDSHIFT_COREDATAPATH"] + + redshift_tool_path = os.path.join( + redshift_os_path, + "bin", + tool_name + ) + + return find_executable(redshift_tool_path) + + def escape_space(path): """Ensure path is enclosed by quotes to allow paths with spaces""" return '"{}"'.format(path) if " " in path else path @@ -605,25 +627,3 @@ class ExtractModelRenderSets(ExtractLook): self.scene_type = self.scene_type_prefix + self.scene_type return typ - - -def get_redshift_tool(tool_name): - """Path to redshift texture processor. - - On Windows it adds .exe extension if missing from tool argument. - - Args: - tool (string): Tool name. - - Returns: - str: Full path to redshift texture processor executable. - """ - redshift_os_path = os.environ["REDSHIFT_COREDATAPATH"] - - redshift_tool_path = os.path.join( - redshift_os_path, - "bin", - tool_name - ) - - return find_executable(redshift_tool_path) From 29b69bcbb805702080fe54e52505fe09a3741f99 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Thu, 9 Jun 2022 20:47:26 +0300 Subject: [PATCH 023/228] Class cleanup --- openpype/hosts/maya/plugins/publish/extract_look.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index d7b179daf2..be31deeb6e 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -69,12 +69,12 @@ def find_paths_by_hash(texture_hash): class TextureProcessor(metaclass=ABC.ABCMeta): def __init__(self): - # TODO: Figure out design for predetermined objects to be initialized. + pass @abstractmethod def process(self, filepath): - return processed_texture_path + pass class MakeRSTexBin(TextureProcessor): @@ -544,8 +544,7 @@ class ExtractLook(openpype.api.Extractor): fname, ext = os.path.splitext(os.path.basename(filepath)) args = [] - if do_maketx: - args.append("maketx") + texture_hash = openpype.api.source_hash(filepath, *args) # If source has been published before with the same settings, @@ -571,7 +570,7 @@ class ExtractLook(openpype.api.Extractor): do_rstex = instance.data.get("rstex", False) if do_rstex: processors.append(MakeRSTexBin) - + if do_maketx and ext != ".tx": # Produce .tx file in staging if source file is not .tx converted = os.path.join(staging, "resources", fname + ".tx") From e78314ca92283b38fd05d8995aeb9c06b460277e Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Thu, 9 Jun 2022 20:49:07 +0300 Subject: [PATCH 024/228] Remove unused maketx code. --- .../maya/plugins/publish/extract_look.py | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index be31deeb6e..3e1f91f5b7 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -544,7 +544,6 @@ class ExtractLook(openpype.api.Extractor): fname, ext = os.path.splitext(os.path.basename(filepath)) args = [] - texture_hash = openpype.api.source_hash(filepath, *args) # If source has been published before with the same settings, @@ -571,32 +570,6 @@ class ExtractLook(openpype.api.Extractor): if do_rstex: processors.append(MakeRSTexBin) - if do_maketx and ext != ".tx": - # Produce .tx file in staging if source file is not .tx - converted = os.path.join(staging, "resources", fname + ".tx") - - if linearize: - self.log.info("tx: converting sRGB -> linear") - colorconvert = "--colorconvert sRGB linear" - else: - colorconvert = "" - - # Ensure folder exists - if not os.path.exists(os.path.dirname(converted)): - os.makedirs(os.path.dirname(converted)) - - self.log.info("Generating .tx file for %s .." % filepath) - maketx( - filepath, - converted, - # Include `source-hash` as string metadata - "-sattrib", - "sourceHash", - escape_space(texture_hash), - colorconvert, - ) - - return converted, COPY, texture_hash self.log.info("Generating .rstexbin file for %s .." % filepath) # Generates Redshift optimized textures using Redshift processor From 00d877e2dfca6b1e89a4b4d289b2621a6e8b2037 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Thu, 9 Jun 2022 20:50:51 +0300 Subject: [PATCH 025/228] Move processors list --- .../maya/plugins/publish/extract_look.py | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 3e1f91f5b7..88c93a8e3b 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -433,7 +433,15 @@ class ExtractLook(openpype.api.Extractor): # if do_maketx: # color_space = "Raw" - + # Specify texture processing executables to activate + processors = [] + do_maketx = instance.data.get("maketx", False) + if do_maketx: + processors.append(MakeTX) + # Option to convert textures to native redshift textures + do_rstex = instance.data.get("rstex", False) + if do_rstex: + processors.append(MakeRSTexBin) source, mode, texture_hash = self._process_texture( filepath, processors, @@ -559,16 +567,6 @@ class ExtractLook(openpype.api.Extractor): ("Paths not found on disk, " "skipping hardlink: %s") % (existing,) ) - texture_files = self.collect_text - # Specify texture processing executables to activate - processors = [] - do_maketx = instance.data.get("maketx", False) - if do_maketx: - processors.append(MakeTX) - # Option to convert textures to native redshift textures - do_rstex = instance.data.get("rstex", False) - if do_rstex: - processors.append(MakeRSTexBin) self.log.info("Generating .rstexbin file for %s .." % filepath) From 2933e409c769f13afd0e1df778fdacd06cf7b848 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Thu, 9 Jun 2022 20:59:19 +0300 Subject: [PATCH 026/228] Handle texture processing through processors separately --- openpype/hosts/maya/plugins/publish/extract_look.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 88c93a8e3b..73d661168a 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -442,6 +442,7 @@ class ExtractLook(openpype.api.Extractor): do_rstex = instance.data.get("rstex", False) if do_rstex: processors.append(MakeRSTexBin) + source, mode, texture_hash = self._process_texture( filepath, processors, @@ -567,12 +568,9 @@ class ExtractLook(openpype.api.Extractor): ("Paths not found on disk, " "skipping hardlink: %s") % (existing,) ) - - - self.log.info("Generating .rstexbin file for %s .." % filepath) - # Generates Redshift optimized textures using Redshift processor - if do_rstex: - rstex(filepath) + for processor in processors: + processor().process(filepath) + self.log.info("Generating .rstexbin file for %s .." % filepath) return filepath, COPY, texture_hash From b054175d6e834dabd7b3d8719eed901ab139062f Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Thu, 9 Jun 2022 21:06:02 +0300 Subject: [PATCH 027/228] Reorganize functionality for do_maketx to linearize properly. --- .../maya/plugins/publish/extract_look.py | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 73d661168a..e90f9759f9 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -426,23 +426,20 @@ class ExtractLook(openpype.api.Extractor): for filepath in files_metadata: linearize = False + + # Specify texture processing executables to activate + processors = [] + do_maketx = instance.data.get("maketx", False) + do_rstex = instance.data.get("rstex", False) + if do_maketx: + processors.append(MakeTX) + # Option to convert textures to native redshift textures + if do_rstex: + processors.append(MakeRSTexBin) if do_maketx and files_metadata[filepath]["color_space"].lower() == "srgb": # noqa: E501 linearize = True # set its file node to 'raw' as tx will be linearized files_metadata[filepath]["color_space"] = "Raw" - - # if do_maketx: - # color_space = "Raw" - # Specify texture processing executables to activate - processors = [] - do_maketx = instance.data.get("maketx", False) - if do_maketx: - processors.append(MakeTX) - # Option to convert textures to native redshift textures - do_rstex = instance.data.get("rstex", False) - if do_rstex: - processors.append(MakeRSTexBin) - source, mode, texture_hash = self._process_texture( filepath, processors, From fd3125deda242f2676f1262fffb4e93212ffa300 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Thu, 9 Jun 2022 21:07:26 +0300 Subject: [PATCH 028/228] Style fixes --- openpype/hosts/maya/plugins/publish/extract_look.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index e90f9759f9..018e45a01f 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -436,6 +436,7 @@ class ExtractLook(openpype.api.Extractor): # Option to convert textures to native redshift textures if do_rstex: processors.append(MakeRSTexBin) + if do_maketx and files_metadata[filepath]["color_space"].lower() == "srgb": # noqa: E501 linearize = True # set its file node to 'raw' as tx will be linearized From 41e7ac78aa340d4af45c48b036fb3ccf39c56f79 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Thu, 9 Jun 2022 22:11:12 +0300 Subject: [PATCH 029/228] adjust comment --- openpype/hosts/maya/plugins/publish/extract_look.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 018e45a01f..922e347561 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -568,7 +568,7 @@ class ExtractLook(openpype.api.Extractor): ) for processor in processors: processor().process(filepath) - self.log.info("Generating .rstexbin file for %s .." % filepath) + self.log.info("Generating texture file for %s .." % filepath) return filepath, COPY, texture_hash From 35be81615ee986b0b71482a8f13097fa469ab477 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Fri, 10 Jun 2022 10:51:19 +0300 Subject: [PATCH 030/228] Fix returned filepath --- openpype/hosts/maya/plugins/publish/extract_look.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 922e347561..7693e91765 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -567,9 +567,9 @@ class ExtractLook(openpype.api.Extractor): "skipping hardlink: %s") % (existing,) ) for processor in processors: - processor().process(filepath) + processed_path = processor().process(filepath) self.log.info("Generating texture file for %s .." % filepath) - return filepath, COPY, texture_hash + return processed_path, COPY, texture_hash class ExtractModelRenderSets(ExtractLook): From cfb90934289a742572d93323d48df6ae061acdb7 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Fri, 10 Jun 2022 20:32:58 +0300 Subject: [PATCH 031/228] Append processors check, append path return. --- openpype/hosts/maya/plugins/publish/extract_look.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 7693e91765..c72aede0d4 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -566,9 +566,13 @@ class ExtractLook(openpype.api.Extractor): ("Paths not found on disk, " "skipping hardlink: %s") % (existing,) ) - for processor in processors: - processed_path = processor().process(filepath) - self.log.info("Generating texture file for %s .." % filepath) + + if bool(processors): + for processor in processors: + processed_path = processor().process(filepath) + self.log.info("Generating texture file for %s .." % filepath) + return processed_path + return processed_path, COPY, texture_hash From eb9994484e94f9e595d95fac302da49cef986cc4 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Fri, 10 Jun 2022 20:35:53 +0300 Subject: [PATCH 032/228] Style fix --- openpype/hosts/maya/plugins/publish/extract_look.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index c72aede0d4..6d8b6f1d8e 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -536,7 +536,7 @@ class ExtractLook(openpype.api.Extractor): resources_dir, basename + ext ) - def _process_texture(self, filepath, processors, staging, linearize, force): + def _process_texture(self, filepath, processors, staging, linearize, force): # noqa """Process a single texture file on disk for publishing. This will: 1. Check whether it's already published, if so it will do hardlink From e5fdd9e9302fe82620b5fb0c94c4ce1188c4a731 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 26 Jul 2022 20:45:54 +0300 Subject: [PATCH 033/228] Syntax fix. --- openpype/hosts/maya/plugins/publish/extract_look.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 6d8b6f1d8e..3fe8139dd6 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """Maya look extractor.""" -from abc import ABC, abstractmethod +import abc import os import sys import json @@ -67,11 +67,11 @@ def find_paths_by_hash(texture_hash): return legacy_io.distinct(key, {"type": "version"}) -class TextureProcessor(metaclass=ABC.ABCMeta): +class TextureProcessor(abc.ABCMeta): def __init__(self): pass - @abstractmethod + @abc.abstractmethod def process(self, filepath): pass From ab14895753c72177ebdfc7ff4f6e8b0bb1eb68ec Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 26 Jul 2022 23:27:11 +0300 Subject: [PATCH 034/228] Change metaclass inheritance formatting to 2.7 --- openpype/hosts/maya/plugins/publish/extract_look.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 3fe8139dd6..be6d863878 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """Maya look extractor.""" -import abc +from abc import ABCMeta, abstractmethod import os import sys import json @@ -67,11 +67,13 @@ def find_paths_by_hash(texture_hash): return legacy_io.distinct(key, {"type": "version"}) -class TextureProcessor(abc.ABCMeta): +class TextureProcessor(object): + __metaclass__ = ABCMeta + def __init__(self): pass - @abc.abstractmethod + @abstractmethod def process(self, filepath): pass From c471bbe71e20f09df30c1ea734a958f3a308e3f6 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Wed, 27 Jul 2022 02:26:18 +0300 Subject: [PATCH 035/228] Fix inheritance with `six`, adjust processing code --- .../maya/plugins/publish/extract_look.py | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index be6d863878..19e0bd9568 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- """Maya look extractor.""" from abc import ABCMeta, abstractmethod +import six import os import sys import json @@ -67,9 +68,9 @@ def find_paths_by_hash(texture_hash): return legacy_io.distinct(key, {"type": "version"}) -class TextureProcessor(object): - __metaclass__ = ABCMeta - +@six.add_metaclass(ABCMeta) +class TextureProcessor: + @abstractmethod def __init__(self): pass @@ -80,7 +81,10 @@ class TextureProcessor(object): class MakeRSTexBin(TextureProcessor): - def process(source, *args): + def __init__(self): + super(TextureProcessor, self).__init__() + + def process(self, source, *args): """Make `.rstexbin` using `redshiftTextureProcessor` with some default settings. @@ -126,7 +130,10 @@ class MakeRSTexBin(TextureProcessor): class MakeTX(TextureProcessor): - def process(source, destination, *args): + def __init__(self): + super(TextureProcessor, self).__init__() + + def process(self, source, destination, *args): """Make `.tx` using `maketx` with some default settings. The settings are based on default as used in Arnold's @@ -558,6 +565,10 @@ class ExtractLook(openpype.api.Extractor): # If source has been published before with the same settings, # then don't reprocess but hardlink from the original existing = find_paths_by_hash(texture_hash) + # if processors["do_maketx"]: + # Produce .tx file in staging if source file is not .tx + + if existing and not force: self.log.info("Found hash in database, preparing hardlink..") source = next((p for p in existing if os.path.exists(p)), None) @@ -571,9 +582,15 @@ class ExtractLook(openpype.api.Extractor): if bool(processors): for processor in processors: - processed_path = processor().process(filepath) - self.log.info("Generating texture file for %s .." % filepath) - return processed_path + if processor == MakeTX: + converted = os.path.join(staging, "resources", fname + ".tx") + processed_path = processor().process(converted, filepath) + self.log.info("Generating texture file for %s .." % filepath) # noqa + return processed_path + elif processor == MakeRSTexBin: + processed_path = processor().process(filepath) + self.log.info("Generating texture file for %s .." % filepath) # noqa + return processed_path return processed_path, COPY, texture_hash From 5f4d06baecce44fd735e1a26770f0a617c0284fb Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Wed, 27 Jul 2022 02:48:43 +0300 Subject: [PATCH 036/228] Remove unnecessary comment, style fixes. --- openpype/hosts/maya/plugins/publish/extract_look.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 19e0bd9568..bb4335a3d5 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -172,7 +172,7 @@ class MakeTX(TextureProcessor): ] cmd.extend(args) - cmd.extend(["-o", escape_space(destination), escape_space(source)]) + cmd.extend([escape_space(source), "-o", escape_space(destination)]) cmd = " ".join(cmd) @@ -188,6 +188,8 @@ class MakeTX(TextureProcessor): import traceback traceback.print_exc() + print(exc.returncode) + print(exc.output) raise return processed_filepath @@ -565,9 +567,6 @@ class ExtractLook(openpype.api.Extractor): # If source has been published before with the same settings, # then don't reprocess but hardlink from the original existing = find_paths_by_hash(texture_hash) - # if processors["do_maketx"]: - # Produce .tx file in staging if source file is not .tx - if existing and not force: self.log.info("Found hash in database, preparing hardlink..") @@ -583,8 +582,8 @@ class ExtractLook(openpype.api.Extractor): if bool(processors): for processor in processors: if processor == MakeTX: - converted = os.path.join(staging, "resources", fname + ".tx") - processed_path = processor().process(converted, filepath) + converted = os.path.join(staging, "resources", fname + ".tx") # noqa + processed_path = processor().process(filepath, converted) self.log.info("Generating texture file for %s .." % filepath) # noqa return processed_path elif processor == MakeRSTexBin: From 9b0cc4dfac23100fa3979b53418734c39399c5ca Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 1 Aug 2022 03:28:26 +0300 Subject: [PATCH 037/228] Continue refactor --- .../maya/plugins/publish/extract_look.py | 52 +++++++++++-------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 136b64a547..8d30268619 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -615,12 +615,29 @@ class ExtractLook(openpype.api.Extractor): ("Paths not found on disk, " "skipping hardlink: %s") % (existing,) ) + config_path = get_ocio_config_path("nuke-default") + color_config = "--colorconfig {0}".format(config_path) + # Ensure folder exists + if linearize: + self.log.info("tx: converting sRGB -> linear") + colorconvert = "--colorconvert sRGB linear" + else: + colorconvert = "" + + converted = os.path.join(staging, "resources", fname + ".tx") + if not os.path.exists(os.path.dirname(converted)): + os.makedirs(os.path.dirname(converted)) if bool(processors): for processor in processors: if processor == MakeTX: - converted = os.path.join(staging, "resources", fname + ".tx") # noqa - processed_path = processor().process(filepath, converted) + processed_path = processor().process(filepath, + converted, + "--sattrib", + "sourceHash %", + escape_space(texture_hash), # noqa + colorconvert, + color_config) self.log.info("Generating texture file for %s .." % filepath) # noqa return processed_path elif processor == MakeRSTexBin: @@ -628,27 +645,18 @@ class ExtractLook(openpype.api.Extractor): self.log.info("Generating texture file for %s .." % filepath) # noqa return processed_path - return processed_path, COPY, texture_hash - config_path = get_ocio_config_path("nuke-default") - color_config = "--colorconfig {0}".format(config_path) - # Ensure folder exists - if not os.path.exists(os.path.dirname(converted)): - os.makedirs(os.path.dirname(converted)) - - self.log.info("Generating .tx file for %s .." % filepath) - maketx( - filepath, - converted, - # Include `source-hash` as string metadata - "--sattrib", - "sourceHash", - escape_space(texture_hash), - colorconvert, - color_config - ) - - return converted, COPY, texture_hash + # self.log.info("Generating .tx file for %s .." % filepath) + # maketx( + # filepath, + # converted, + # # Include `source-hash` as string metadata + # "--sattrib", + # "sourceHash", + # escape_space(texture_hash), + # colorconvert, + # color_config + # ) return filepath, COPY, texture_hash From c35f1cfe3c295d7915e4a2b856f48ecae85cab38 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 1 Aug 2022 04:09:09 +0300 Subject: [PATCH 038/228] Remove leftover code --- .../maya/plugins/publish/extract_look.py | 32 +------------------ 1 file changed, 1 insertion(+), 31 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 8d30268619..ecbb070916 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -114,27 +114,10 @@ class MakeRSTexBin(TextureProcessor): This function requires the `REDSHIFT_COREDATAPATH` to be in `PATH`. - cmd = [ - maketx_path, - "-v", # verbose - "-u", # update mode - # unpremultiply before conversion (recommended when alpha present) - "--unpremult", - "--checknan", - # use oiio-optimized settings for tile-size, planarconfig, metadata - "--oiio", - "--filter lanczos3", - escape_space(source) - ] Args: source (str): Path to source file. *args: Additional arguments for `redshiftTextureProcessor`. - cmd.extend(args) - cmd.extend(["-o", escape_space(destination)]) - Returns: - str: Output of `redshiftTextureProcessor` command. - """ if "REDSHIFT_COREDATAPATH" not in os.environ: raise RuntimeError("Must have Redshift available.") @@ -634,7 +617,7 @@ class ExtractLook(openpype.api.Extractor): processed_path = processor().process(filepath, converted, "--sattrib", - "sourceHash %", + "sourceHash", escape_space(texture_hash), # noqa colorconvert, color_config) @@ -645,19 +628,6 @@ class ExtractLook(openpype.api.Extractor): self.log.info("Generating texture file for %s .." % filepath) # noqa return processed_path - - # self.log.info("Generating .tx file for %s .." % filepath) - # maketx( - # filepath, - # converted, - # # Include `source-hash` as string metadata - # "--sattrib", - # "sourceHash", - # escape_space(texture_hash), - # colorconvert, - # color_config - # ) - return filepath, COPY, texture_hash From 099dfba8fa19dca4bc1cc9a30632f659854c9300 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 1 Aug 2022 05:06:58 +0300 Subject: [PATCH 039/228] Fix return bug --- openpype/hosts/maya/plugins/publish/extract_look.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index ecbb070916..53b6dcbf35 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -622,11 +622,11 @@ class ExtractLook(openpype.api.Extractor): colorconvert, color_config) self.log.info("Generating texture file for %s .." % filepath) # noqa - return processed_path + return processed_path, COPY, texture_hash elif processor == MakeRSTexBin: processed_path = processor().process(filepath) self.log.info("Generating texture file for %s .." % filepath) # noqa - return processed_path + return processed_path, COPY, texture_hash return filepath, COPY, texture_hash From 8995dcdff4a9a6c5c951958fb71b99ee93b44029 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 1 Aug 2022 12:26:44 +0300 Subject: [PATCH 040/228] Check for return value, adjust argument --- .../hosts/maya/plugins/publish/extract_look.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 53b6dcbf35..48af644326 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- """Maya look extractor.""" from abc import ABCMeta, abstractmethod + import six import os import sys @@ -189,10 +190,11 @@ class MakeTX(TextureProcessor): # use oiio-optimized settings for tile-size, planarconfig, metadata "--oiio", "--filter lanczos3", + escape_space(source) ] cmd.extend(args) - cmd.extend([escape_space(source), "-o", escape_space(destination)]) + cmd.extend(["-o", escape_space(destination)]) cmd = " ".join(cmd) @@ -620,13 +622,21 @@ class ExtractLook(openpype.api.Extractor): "sourceHash", escape_space(texture_hash), # noqa colorconvert, - color_config) + color_config, + ) self.log.info("Generating texture file for %s .." % filepath) # noqa - return processed_path, COPY, texture_hash + self.log.info(converted) + if processed_path: + return processed_path + else: + self.log.info("maketx has returned nothing") elif processor == MakeRSTexBin: processed_path = processor().process(filepath) self.log.info("Generating texture file for %s .." % filepath) # noqa - return processed_path, COPY, texture_hash + if processed_path: + return processed_path + else: + self.log.info("redshift texture converter has returned nothing") # noqa return filepath, COPY, texture_hash From 3cd7fd503c0cb94ef3f4da86a3c465ff6bc26cb1 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 15 Aug 2022 04:23:24 +0300 Subject: [PATCH 041/228] Fix assignment bug, remove unnecessary class sugar --- openpype/hosts/maya/plugins/publish/extract_look.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 48af644326..c092e2ac25 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -94,7 +94,7 @@ def find_paths_by_hash(texture_hash): @six.add_metaclass(ABCMeta) class TextureProcessor: - @abstractmethod + def __init__(self): pass @@ -106,7 +106,7 @@ class TextureProcessor: class MakeRSTexBin(TextureProcessor): def __init__(self): - super(TextureProcessor, self).__init__() + super(MakeRSTexBin, self).__init__() def process(self, source, *args): """Make `.rstexbin` using `redshiftTextureProcessor` @@ -152,7 +152,7 @@ class MakeRSTexBin(TextureProcessor): class MakeTX(TextureProcessor): def __init__(self): - super(TextureProcessor, self).__init__() + super(MakeTX, self).__init__() def process(self, source, destination, *args): """Make `.tx` using `maketx` with some default settings. @@ -627,18 +627,18 @@ class ExtractLook(openpype.api.Extractor): self.log.info("Generating texture file for %s .." % filepath) # noqa self.log.info(converted) if processed_path: - return processed_path + return processed_path, COPY, texture_hash else: self.log.info("maketx has returned nothing") elif processor == MakeRSTexBin: processed_path = processor().process(filepath) self.log.info("Generating texture file for %s .." % filepath) # noqa if processed_path: - return processed_path + return processed_path, COPY, texture_hash else: self.log.info("redshift texture converter has returned nothing") # noqa - return filepath, COPY, texture_hash + return processed_path, COPY, texture_hash class ExtractModelRenderSets(ExtractLook): From 9ec42cb4376e0e14eb990cfd22c6a9f1c38e0575 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 23 Aug 2022 13:05:37 +0300 Subject: [PATCH 042/228] Fix bugs --- .../hosts/maya/plugins/publish/extract_look.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 52751eea81..19765d4396 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -125,7 +125,7 @@ class MakeRSTexBin(TextureProcessor): if "REDSHIFT_COREDATAPATH" not in os.environ: raise RuntimeError("Must have Redshift available.") - texture_processor_path = get_redshift_tool("TextureProcessor") + texture_processor_path = get_redshift_tool("redshiftTextureProcessor") cmd = [ texture_processor_path, @@ -142,14 +142,14 @@ class MakeRSTexBin(TextureProcessor): if sys.platform == "win32": kwargs["creationflags"] = CREATE_NO_WINDOW try: - processed_filepath = subprocess.check_output(**kwargs) + subprocess.check_output(**kwargs) except subprocess.CalledProcessError as exc: print(exc) import traceback traceback.print_exc() raise - return processed_filepath + return source class MakeTX(TextureProcessor): @@ -206,7 +206,7 @@ class MakeTX(TextureProcessor): if sys.platform == "win32": kwargs["creationflags"] = CREATE_NO_WINDOW try: - processed_filepath = subprocess.check_output(**kwargs) + subprocess.check_output(**kwargs) except subprocess.CalledProcessError as exc: print(exc) import traceback @@ -216,7 +216,7 @@ class MakeTX(TextureProcessor): print(exc.output) raise - return processed_filepath + return destination @contextlib.contextmanager @@ -629,7 +629,7 @@ class ExtractLook(openpype.api.Extractor): if bool(processors): for processor in processors: - if processor == MakeTX: + if processor is MakeTX: processed_path = processor().process(filepath, converted, "--sattrib", @@ -644,7 +644,7 @@ class ExtractLook(openpype.api.Extractor): return processed_path, COPY, texture_hash else: self.log.info("maketx has returned nothing") - elif processor == MakeRSTexBin: + elif processor is MakeRSTexBin: processed_path = processor().process(filepath) self.log.info("Generating texture file for %s .." % filepath) # noqa if processed_path: From cc62035e1be2e02d38c3e1972bd37f458f70eb83 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 23 Aug 2022 13:22:59 +0300 Subject: [PATCH 043/228] Fix destination finding for copying resrouce. --- .../maya/plugins/publish/extract_look.py | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 19765d4396..7f79867cea 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -483,9 +483,15 @@ class ExtractLook(openpype.api.Extractor): linearize=linearize, force=force_copy ) - destination = self.resource_destination(instance, - source, - do_maketx) + for processor in processors: + if processor is MakeTX: + destination = self.resource_destination( + instance, source, MakeTX + ) + elif processor is MakeRSTexBin: + destination = self.resource_destination( + instance, source, MakeRSTexBin + ) # Force copy is specified. if force_copy: @@ -512,9 +518,15 @@ class ExtractLook(openpype.api.Extractor): if source not in destinations: # Cache destination as source resource might be included # multiple times - destinations[source] = self.resource_destination( - instance, source, do_maketx - ) + for processor in processors: + if processor is MakeTX: + destinations[source] = self.resource_destination( + instance, source, MakeTX + ) + elif processor is MakeRSTexBin: + destinations[source] = self.resource_destination( + instance, source, MakeRSTexBin + ) # Preserve color space values (force value after filepath change) # This will also trigger in the same order at end of context to @@ -555,7 +567,7 @@ class ExtractLook(openpype.api.Extractor): "attrRemap": remap, } - def resource_destination(self, instance, filepath, do_maketx): + def resource_destination(self, instance, filepath, processor): """Get resource destination path. This is utility function to change path if resource file name is @@ -564,7 +576,7 @@ class ExtractLook(openpype.api.Extractor): Args: instance: Current Instance. filepath (str): Resource path - do_maketx (bool): Flag if resource is processed by `maketx`. + processor: Texture processor converting resource. Returns: str: Path to resource file @@ -576,8 +588,10 @@ class ExtractLook(openpype.api.Extractor): basename, ext = os.path.splitext(os.path.basename(filepath)) # If `maketx` then the texture will always end with .tx - if do_maketx: + if processor == MakeTX: ext = ".tx" + elif processor == MakeRSTexBin: + ext = ".rstexbin" return os.path.join( resources_dir, basename + ext From ad8177cee3c7ee6241b995b781b9dc93fead19b1 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 23 Aug 2022 16:10:44 +0300 Subject: [PATCH 044/228] Removed submodule vendor/configs/OpenColorIO-Configs --- vendor/configs/OpenColorIO-Configs | 1 - 1 file changed, 1 deletion(-) delete mode 160000 vendor/configs/OpenColorIO-Configs diff --git a/vendor/configs/OpenColorIO-Configs b/vendor/configs/OpenColorIO-Configs deleted file mode 160000 index 0bb079c08b..0000000000 --- a/vendor/configs/OpenColorIO-Configs +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0bb079c08be410030669cbf5f19ff869b88af953 From 590538eb128dc8e7e8e6eee0b8d16ab593530e5e Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 4 Oct 2022 11:07:09 +0300 Subject: [PATCH 045/228] Fix import bug --- openpype/hosts/maya/plugins/publish/extract_look.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 7e640c375d..f3128306ee 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -11,13 +11,14 @@ import platform import contextlib import subprocess from openpype.lib.vendor_bin_utils import find_executable +from openpype.lib import source_hash from collections import OrderedDict from maya import cmds # noqa import pyblish.api -from openpype.lib import source_hash, run_subprocess + from openpype.pipeline import legacy_io, publish from openpype.hosts.maya.api import lib @@ -93,7 +94,6 @@ def find_paths_by_hash(texture_hash): key = "data.sourceHashes.{0}".format(texture_hash) return legacy_io.distinct(key, {"type": "version"}) - @six.add_metaclass(ABCMeta) class TextureProcessor: @@ -612,7 +612,7 @@ class ExtractLook(publish.Extractor): fname, ext = os.path.splitext(os.path.basename(filepath)) args = [] - texture_hash = openpype.api.source_hash(filepath, *args) + texture_hash = source_hash(filepath, *args) # If source has been published before with the same settings, # then don't reprocess but hardlink from the original From 035281fa4c939e6e56d356a97535cf837cc933a2 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 4 Oct 2022 11:08:27 +0300 Subject: [PATCH 046/228] Style fix --- openpype/hosts/maya/plugins/publish/extract_look.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index f3128306ee..6b0fe36d8f 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -94,6 +94,7 @@ def find_paths_by_hash(texture_hash): key = "data.sourceHashes.{0}".format(texture_hash) return legacy_io.distinct(key, {"type": "version"}) + @six.add_metaclass(ABCMeta) class TextureProcessor: From 18435fa7065065d8446c3b0448f664f9ef4a8123 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 4 Oct 2022 14:11:30 +0300 Subject: [PATCH 047/228] Add validation to check for texture --- .../plugins/publish/validate_look_contents.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/validate_look_contents.py b/openpype/hosts/maya/plugins/publish/validate_look_contents.py index 53501d11e5..837e8ea3a2 100644 --- a/openpype/hosts/maya/plugins/publish/validate_look_contents.py +++ b/openpype/hosts/maya/plugins/publish/validate_look_contents.py @@ -1,6 +1,7 @@ import pyblish.api import openpype.hosts.maya.api.action from openpype.pipeline.publish import ValidateContentsOrder +from maya import cmds # noqa class ValidateLookContents(pyblish.api.InstancePlugin): @@ -85,6 +86,7 @@ class ValidateLookContents(pyblish.api.InstancePlugin): invalid.add(instance.name) return list(invalid) + @classmethod def validate_looks(cls, instance): @@ -112,3 +114,23 @@ class ValidateLookContents(pyblish.api.InstancePlugin): invalid.append(node) return invalid + + @classmethod + def validate_renderer(cls, instance): + + renderer = cmds.getAttr( + 'defaultRenderGlobals.currentRenderer').lower() + do_maketx = instance.data.get("maketx", False) + do_rstex = instance.data.get("rstex", False) + processors = [] + + if do_maketx: + processors.append('arnold') + if do_rstex: + processors.append('redshift') + + for processor in processors: + if processor == renderer: + continue + else: + cls.log.error("Converted texture does not match current renderer.") # noqa From 3728ce0a586820bd0e7619dd7acc5cff82e0f918 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 4 Oct 2022 14:16:54 +0300 Subject: [PATCH 048/228] Style fix --- openpype/hosts/maya/plugins/publish/validate_look_contents.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_look_contents.py b/openpype/hosts/maya/plugins/publish/validate_look_contents.py index 837e8ea3a2..5e242ed3d2 100644 --- a/openpype/hosts/maya/plugins/publish/validate_look_contents.py +++ b/openpype/hosts/maya/plugins/publish/validate_look_contents.py @@ -119,7 +119,7 @@ class ValidateLookContents(pyblish.api.InstancePlugin): def validate_renderer(cls, instance): renderer = cmds.getAttr( - 'defaultRenderGlobals.currentRenderer').lower() + 'defaultRenderGlobals.currentRenderer').lower() do_maketx = instance.data.get("maketx", False) do_rstex = instance.data.get("rstex", False) processors = [] From 00f2e16dca2cb37a53a4a6b3096c0f11142a9b66 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Sun, 26 Feb 2023 10:23:39 +0000 Subject: [PATCH 049/228] Static implementation. --- openpype/hosts/maya/plugins/publish/collect_review.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/collect_review.py b/openpype/hosts/maya/plugins/publish/collect_review.py index eb872c2935..c76d4bae97 100644 --- a/openpype/hosts/maya/plugins/publish/collect_review.py +++ b/openpype/hosts/maya/plugins/publish/collect_review.py @@ -139,3 +139,13 @@ class CollectReview(pyblish.api.InstancePlugin): "filename": node.filename.get() } ) + + # Collect focal length. + data = { + "cameraFocalLength": cmds.getAttr(camera + ".focalLength") + } + + try: + instance.data["customData"].update(data) + except KeyError: + instance.data["customData"] = data From 0483714dcb55f2f7b2db5892c640ef6b673f393f Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Mon, 13 Mar 2023 17:49:44 +0100 Subject: [PATCH 050/228] Change "Maya" to Fusion in comment --- openpype/hosts/fusion/api/action.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/fusion/api/action.py b/openpype/hosts/fusion/api/action.py index 1750920950..ff5dd14caa 100644 --- a/openpype/hosts/fusion/api/action.py +++ b/openpype/hosts/fusion/api/action.py @@ -6,12 +6,13 @@ from openpype.pipeline.publish import get_errored_instances_from_context class SelectInvalidAction(pyblish.api.Action): - """Select invalid nodes in Maya when plug-in failed. + """Select invalid nodes in Fusion when plug-in failed. To retrieve the invalid nodes this assumes a static `get_invalid()` method is available on the plugin. """ + label = "Select invalid" on = "failed" # This action is only available on a failed plug-in icon = "search" # Icon from Awesome Icon @@ -31,8 +32,10 @@ class SelectInvalidAction(pyblish.api.Action): if isinstance(invalid_nodes, (list, tuple)): invalid.extend(invalid_nodes) else: - self.log.warning("Plug-in returned to be invalid, " - "but has no selectable nodes.") + self.log.warning( + "Plug-in returned to be invalid, " + "but has no selectable nodes." + ) if not invalid: # Assume relevant comp is current comp and clear selection @@ -51,4 +54,6 @@ class SelectInvalidAction(pyblish.api.Action): for tool in invalid: flow.Select(tool, True) names.add(tool.Name) - self.log.info("Selecting invalid tools: %s" % ", ".join(sorted(names))) + self.log.info( + "Selecting invalid tools: %s" % ", ".join(sorted(names)) + ) From ea7390d1d3e91385cd7f723c3067cebabf5cca03 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Mon, 13 Mar 2023 17:50:56 +0100 Subject: [PATCH 051/228] Added create and instance attr to Create plugin Where they get it from should be moved into another folder so other plugins can get the same data. --- .../fusion/plugins/create/create_saver.py | 83 +++++++++++++------ 1 file changed, 56 insertions(+), 27 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index e581bac20f..56085b0a06 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -4,29 +4,34 @@ import qtawesome from openpype.hosts.fusion.api import ( get_current_comp, - comp_lock_and_undo_chunk + comp_lock_and_undo_chunk, ) -from openpype.lib import BoolDef +from openpype.lib import ( + BoolDef, + EnumDef, +) from openpype.pipeline import ( legacy_io, Creator, - CreatedInstance + CreatedInstance, +) +from openpype.client import ( + get_asset_by_name, ) -from openpype.client import get_asset_by_name class CreateSaver(Creator): identifier = "io.openpype.creators.fusion.saver" - name = "saver" - label = "Saver" + label = "Render (saver)" + name = "render" family = "render" - default_variants = ["Main"] - + default_variants = ["Main", "Mask"] description = "Fusion Saver to generate image sequence" - def create(self, subset_name, instance_data, pre_create_data): + instance_attributes = ["reviewable"] + def create(self, subset_name, instance_data, pre_create_data): # TODO: Add pre_create attributes to choose file format? file_format = "OpenEXRFormat" @@ -58,7 +63,8 @@ class CreateSaver(Creator): family=self.family, subset_name=subset_name, data=instance_data, - creator=self) + creator=self, + ) # Insert the transient data instance.transient_data["tool"] = saver @@ -68,11 +74,9 @@ class CreateSaver(Creator): return instance def collect_instances(self): - comp = get_current_comp() tools = comp.GetToolList(False, "Saver").values() for tool in tools: - data = self.get_managed_tool_data(tool) if not data: data = self._collect_unmanaged_saver(tool) @@ -90,7 +94,6 @@ class CreateSaver(Creator): def update_instances(self, update_list): for created_inst, _changes in update_list: - new_data = created_inst.data_to_store() tool = created_inst.transient_data["tool"] self._update_tool_with_data(tool, new_data) @@ -139,7 +142,6 @@ class CreateSaver(Creator): tool.SetAttrs({"TOOLS_Name": subset}) def _collect_unmanaged_saver(self, tool): - # TODO: this should not be done this way - this should actually # get the data as stored on the tool explicitly (however) # that would disallow any 'regular saver' to be collected @@ -153,8 +155,7 @@ class CreateSaver(Creator): asset = legacy_io.Session["AVALON_ASSET"] task = legacy_io.Session["AVALON_TASK"] - asset_doc = get_asset_by_name(project_name=project, - asset_name=asset) + asset_doc = get_asset_by_name(project_name=project, asset_name=asset) path = tool["Clip"][comp.TIME_UNDEFINED] fname = os.path.basename(path) @@ -178,21 +179,20 @@ class CreateSaver(Creator): "variant": variant, "active": not passthrough, "family": self.family, - # Unique identifier for instance and this creator "id": "pyblish.avalon.instance", - "creator_identifier": self.identifier + "creator_identifier": self.identifier, } def get_managed_tool_data(self, tool): """Return data of the tool if it matches creator identifier""" - data = tool.GetData('openpype') + data = tool.GetData("openpype") if not isinstance(data, dict): return required = { "id": "pyblish.avalon.instance", - "creator_identifier": self.identifier + "creator_identifier": self.identifier, } for key, value in required.items(): if key not in data or data[key] != value: @@ -205,11 +205,40 @@ class CreateSaver(Creator): return data - def get_instance_attr_defs(self): - return [ - BoolDef( - "review", - default=True, - label="Review" - ) + def get_pre_create_attr_defs(self): + """Settings for create page""" + attr_defs = [ + self._get_render_target_enum(), + self._get_reviewable_bool(), ] + return attr_defs + + def get_instance_attr_defs(self): + """Settings for publish page""" + attr_defs = [ + self._get_render_target_enum(), + self._get_reviewable_bool(), + ] + return attr_defs + + # 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", + "frames": "Use existing frames", + } + if "farm_rendering" in self.instance_attributes: + rendering_targets["farm"] = "Farm rendering" + + return EnumDef( + "render_target", items=rendering_targets, label="Render target" + ) + + def _get_reviewable_bool(self): + return BoolDef( + "review", + default=("reviewable" in self.instance_attributes), + label="Review", + ) From 0414045f362bdd6c2ecfeb5851b82f129de4f710 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Mon, 13 Mar 2023 17:54:02 +0100 Subject: [PATCH 052/228] Split the render into render and review data --- .../plugins/publish/collect_render_target.py | 44 --------------- .../fusion/plugins/publish/collect_renders.py | 42 ++++++++++++++ ...ender_local.py => extract_render_local.py} | 46 ++-------------- .../plugins/publish/extract_review_data.py | 55 +++++++++++++++++++ 4 files changed, 102 insertions(+), 85 deletions(-) delete mode 100644 openpype/hosts/fusion/plugins/publish/collect_render_target.py create mode 100644 openpype/hosts/fusion/plugins/publish/collect_renders.py rename openpype/hosts/fusion/plugins/publish/{render_local.py => extract_render_local.py} (59%) create mode 100644 openpype/hosts/fusion/plugins/publish/extract_review_data.py diff --git a/openpype/hosts/fusion/plugins/publish/collect_render_target.py b/openpype/hosts/fusion/plugins/publish/collect_render_target.py deleted file mode 100644 index 39017f32e0..0000000000 --- a/openpype/hosts/fusion/plugins/publish/collect_render_target.py +++ /dev/null @@ -1,44 +0,0 @@ -import pyblish.api - - -class CollectFusionRenderMode(pyblish.api.InstancePlugin): - """Collect current comp's render Mode - - Options: - local - farm - - Note that this value is set for each comp separately. When you save the - comp this information will be stored in that file. If for some reason the - available tool does not visualize which render mode is set for the - current comp, please run the following line in the console (Py2) - - comp.GetData("openpype.rendermode") - - This will return the name of the current render mode as seen above under - Options. - - """ - - order = pyblish.api.CollectorOrder + 0.4 - label = "Collect Render Mode" - hosts = ["fusion"] - families = ["render"] - - def process(self, instance): - """Collect all image sequence tools""" - options = ["local", "farm"] - - comp = instance.context.data.get("currentComp") - if not comp: - raise RuntimeError("No comp previously collected, unable to " - "retrieve Fusion version.") - - rendermode = comp.GetData("openpype.rendermode") or "local" - assert rendermode in options, "Must be supported render mode" - - self.log.info("Render mode: {0}".format(rendermode)) - - # Append family - family = "render.{0}".format(rendermode) - instance.data["families"].append(family) diff --git a/openpype/hosts/fusion/plugins/publish/collect_renders.py b/openpype/hosts/fusion/plugins/publish/collect_renders.py new file mode 100644 index 0000000000..cdd37b3b65 --- /dev/null +++ b/openpype/hosts/fusion/plugins/publish/collect_renders.py @@ -0,0 +1,42 @@ +import pyblish.api +from pprint import pformat + + +class CollectFusionRenders(pyblish.api.InstancePlugin): + """Collect current comp's render Mode + + Options: + local + farm + + Note that this value is set for each comp separately. When you save the + comp this information will be stored in that file. If for some reason the + available tool does not visualize which render mode is set for the + current comp, please run the following line in the console (Py2) + + comp.GetData("openpype.rendermode") + + This will return the name of the current render mode as seen above under + Options. + + """ + + order = pyblish.api.CollectorOrder + 0.4 + label = "Collect Renders" + hosts = ["fusion"] + families = ["render"] + + def process(self, instance): + self.log.debug(pformat(instance.data)) + + saver_node = instance.data["transientData"]["tool"] + render_target = instance.data["render_target"] + family = instance.data["family"] + families = instance.data["families"] + + # add targeted family to families + instance.data["families"].append( + "{}.{}".format(family, render_target) + ) + + self.log.debug(pformat(instance.data)) diff --git a/openpype/hosts/fusion/plugins/publish/render_local.py b/openpype/hosts/fusion/plugins/publish/extract_render_local.py similarity index 59% rename from openpype/hosts/fusion/plugins/publish/render_local.py rename to openpype/hosts/fusion/plugins/publish/extract_render_local.py index 7d5f1a40c7..62625c899a 100644 --- a/openpype/hosts/fusion/plugins/publish/render_local.py +++ b/openpype/hosts/fusion/plugins/publish/extract_render_local.py @@ -4,16 +4,12 @@ from openpype.pipeline import publish from openpype.hosts.fusion.api import comp_lock_and_undo_chunk -class Fusionlocal(pyblish.api.InstancePlugin, - publish.ColormanagedPyblishPluginMixin): - """Render the current Fusion composition locally. +class FusionRenderLocal( + pyblish.api.InstancePlugin, publish.ColormanagedPyblishPluginMixin +): + """Render the current Fusion composition locally.""" - Extract the result of savers by starting a comp render - This will run the local render of Fusion. - - """ - - order = pyblish.api.ExtractorOrder - 0.1 + order = pyblish.api.ExtractorOrder - 0.2 label = "Render Local" hosts = ["fusion"] families = ["render.local"] @@ -33,38 +29,6 @@ class Fusionlocal(pyblish.api.InstancePlugin, ) ) - 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, - ) - - if "representations" not in instance.data: - instance.data["representations"] = [] - instance.data["representations"].append(repre) - - # review representation - if instance.data.get("review", False): - repre["tags"] = ["review", "ftrackreview"] - def render_once(self, context): """Render context comp only once, even with more render instances""" diff --git a/openpype/hosts/fusion/plugins/publish/extract_review_data.py b/openpype/hosts/fusion/plugins/publish/extract_review_data.py new file mode 100644 index 0000000000..d9416771f6 --- /dev/null +++ b/openpype/hosts/fusion/plugins/publish/extract_review_data.py @@ -0,0 +1,55 @@ +import os +import pyblish.api +from openpype.pipeline.publish import ( + ColormanagedPyblishPluginMixin, +) + + +class FusionExtractReviewData( + pyblish.api.InstancePlugin, ColormanagedPyblishPluginMixin +): + """ + Extract the result of savers by starting a comp render + This will run the local render of Fusion. + """ + + order = pyblish.api.ExtractorOrder - 0.1 + label = "Extract Review Data" + hosts = ["fusion"] + families = ["review"] + + 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) From ba8d03715229693745ef5df4525c6eb0fc156bf2 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Mon, 13 Mar 2023 17:54:47 +0100 Subject: [PATCH 053/228] Validate that frames for "use existing frames" do exist --- .../validate_local_frames_existence.py | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 openpype/hosts/fusion/plugins/publish/validate_local_frames_existence.py diff --git a/openpype/hosts/fusion/plugins/publish/validate_local_frames_existence.py b/openpype/hosts/fusion/plugins/publish/validate_local_frames_existence.py new file mode 100644 index 0000000000..d80e11e078 --- /dev/null +++ b/openpype/hosts/fusion/plugins/publish/validate_local_frames_existence.py @@ -0,0 +1,71 @@ +import os +import pyblish.api + +from openpype.pipeline.publish import RepairAction +from openpype.pipeline import PublishValidationError + +from openpype.hosts.fusion.api.action import SelectInvalidAction + + +class ValidateLocalFramesExistence(pyblish.api.InstancePlugin): + """Checks if files for savers that's set to publish existing frames exists""" + + order = pyblish.api.ValidatorOrder + label = "Validate Existing Frames Exists" + families = ["render"] + hosts = ["fusion"] + actions = [RepairAction, SelectInvalidAction] + + @classmethod + def get_invalid(cls, instance): + active = instance.data.get("active", instance.data.get("publish")) + if not active: + return [] + + if instance.data.get("render_target") == "frames": + tool = instance[0] + + frame_start = instance.data["frameStart"] + frame_end = instance.data["frameEnd"] + 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) + ] + + non_existing_frames = [] + + for file in files: + cls.log.error(file) + if not os.path.exists(os.path.join(output_dir, file)): + non_existing_frames.append(file) + + if len(non_existing_frames) > 0: + cls.log.error( + "Some of {}'s files does not exist".format(tool.Name) + ) + return [tool, output_dir, non_existing_frames] + + def process(self, instance): + invalid = self.get_invalid(instance) + if invalid: + raise PublishValidationError( + "{} is set to publish existing frames but " + "some frames are missing in the folder:\n\n{}" + "The missing file(s) are:\n\n{}".format( + invalid[0].Name, + invalid[1], + "\n\n".join(invalid[2]), + ), + title=self.label, + ) + + @classmethod + def repair(cls, instance): + invalid = cls.get_invalid(instance) + for tool in invalid: + tool.SetInput("CreateDir", 1.0) From a15e29afaf5ff8e78296f36cea9f06d036e1cccf Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Mon, 13 Mar 2023 17:55:04 +0100 Subject: [PATCH 054/228] Fixed repair-button to show up on review tab --- .../plugins/publish/validate_create_folder_checked.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 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 ba943abacb..53dea2af71 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py +++ b/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py @@ -14,11 +14,10 @@ class ValidateCreateFolderChecked(pyblish.api.InstancePlugin): """ order = pyblish.api.ValidatorOrder - actions = [RepairAction] label = "Validate Create Folder Checked" families = ["render"] hosts = ["fusion"] - actions = [SelectInvalidAction] + actions = [RepairAction, SelectInvalidAction] @classmethod def get_invalid(cls, instance): @@ -29,7 +28,9 @@ class ValidateCreateFolderChecked(pyblish.api.InstancePlugin): tool = instance[0] create_dir = tool.GetInput("CreateDir") if create_dir == 0.0: - cls.log.error("%s has Create Folder turned off" % instance[0].Name) + cls.log.error( + "%s has Create Folder turned off" % instance[0].Name + ) return [tool] def process(self, instance): @@ -37,7 +38,8 @@ class ValidateCreateFolderChecked(pyblish.api.InstancePlugin): if invalid: raise PublishValidationError( "Found Saver with Create Folder During Render checked off", - title=self.label) + title=self.label, + ) @classmethod def repair(cls, instance): From 73b6ee58ae61b6ee88d6cd6bd28f78c6610220f1 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Mon, 13 Mar 2023 17:57:09 +0100 Subject: [PATCH 055/228] Remove old Render Mode menu --- openpype/hosts/fusion/api/menu.py | 12 -- .../hosts/fusion/scripts/set_rendermode.py | 112 ------------------ 2 files changed, 124 deletions(-) delete mode 100644 openpype/hosts/fusion/scripts/set_rendermode.py diff --git a/openpype/hosts/fusion/api/menu.py b/openpype/hosts/fusion/api/menu.py index 343f5f803a..ba3ce6798d 100644 --- a/openpype/hosts/fusion/api/menu.py +++ b/openpype/hosts/fusion/api/menu.py @@ -60,7 +60,6 @@ class OpenPypeMenu(QtWidgets.QWidget): publish_btn = QtWidgets.QPushButton("Publish...", self) manager_btn = QtWidgets.QPushButton("Manage...", self) libload_btn = QtWidgets.QPushButton("Library...", self) - rendermode_btn = QtWidgets.QPushButton("Set render mode...", self) set_framerange_btn = QtWidgets.QPushButton("Set Frame Range", self) set_resolution_btn = QtWidgets.QPushButton("Set Resolution", self) duplicate_with_inputs_btn = QtWidgets.QPushButton( @@ -91,7 +90,6 @@ class OpenPypeMenu(QtWidgets.QWidget): layout.addWidget(set_framerange_btn) layout.addWidget(set_resolution_btn) - layout.addWidget(rendermode_btn) layout.addSpacing(20) @@ -108,7 +106,6 @@ class OpenPypeMenu(QtWidgets.QWidget): load_btn.clicked.connect(self.on_load_clicked) manager_btn.clicked.connect(self.on_manager_clicked) libload_btn.clicked.connect(self.on_libload_clicked) - rendermode_btn.clicked.connect(self.on_rendermode_clicked) duplicate_with_inputs_btn.clicked.connect( self.on_duplicate_with_inputs_clicked ) @@ -162,15 +159,6 @@ class OpenPypeMenu(QtWidgets.QWidget): def on_libload_clicked(self): host_tools.show_library_loader() - def on_rendermode_clicked(self): - if self.render_mode_widget is None: - window = set_rendermode.SetRenderMode() - window.setStyleSheet(load_stylesheet()) - window.show() - self.render_mode_widget = window - else: - self.render_mode_widget.show() - def on_duplicate_with_inputs_clicked(self): duplicate_with_inputs.duplicate_with_input_connections() diff --git a/openpype/hosts/fusion/scripts/set_rendermode.py b/openpype/hosts/fusion/scripts/set_rendermode.py deleted file mode 100644 index 9d2bfef310..0000000000 --- a/openpype/hosts/fusion/scripts/set_rendermode.py +++ /dev/null @@ -1,112 +0,0 @@ -from qtpy import QtWidgets -import qtawesome -from openpype.hosts.fusion.api import get_current_comp - - -_help = {"local": "Render the comp on your own machine and publish " - "it from that the destination folder", - "farm": "Submit a Fusion render job to a Render farm to use all other" - " computers and add a publish job"} - - -class SetRenderMode(QtWidgets.QWidget): - - def __init__(self, parent=None): - QtWidgets.QWidget.__init__(self, parent) - - self._comp = get_current_comp() - self._comp_name = self._get_comp_name() - - self.setWindowTitle("Set Render Mode") - self.setFixedSize(300, 175) - - layout = QtWidgets.QVBoxLayout() - - # region comp info - comp_info_layout = QtWidgets.QHBoxLayout() - - update_btn = QtWidgets.QPushButton(qtawesome.icon("fa.refresh", - color="white"), "") - update_btn.setFixedWidth(25) - update_btn.setFixedHeight(25) - - comp_information = QtWidgets.QLineEdit() - comp_information.setEnabled(False) - - comp_info_layout.addWidget(comp_information) - comp_info_layout.addWidget(update_btn) - # endregion comp info - - # region modes - mode_options = QtWidgets.QComboBox() - mode_options.addItems(_help.keys()) - - mode_information = QtWidgets.QTextEdit() - mode_information.setReadOnly(True) - # endregion modes - - accept_btn = QtWidgets.QPushButton("Accept") - - layout.addLayout(comp_info_layout) - layout.addWidget(mode_options) - layout.addWidget(mode_information) - layout.addWidget(accept_btn) - - self.setLayout(layout) - - self.comp_information = comp_information - self.update_btn = update_btn - - self.mode_options = mode_options - self.mode_information = mode_information - - self.accept_btn = accept_btn - - self.connections() - self.update() - - # Force updated render mode help text - self._update_rendermode_info() - - def connections(self): - """Build connections between code and buttons""" - - self.update_btn.clicked.connect(self.update) - self.accept_btn.clicked.connect(self._set_comp_rendermode) - self.mode_options.currentIndexChanged.connect( - self._update_rendermode_info) - - def update(self): - """Update all information in the UI""" - - self._comp = get_current_comp() - self._comp_name = self._get_comp_name() - self.comp_information.setText(self._comp_name) - - # Update current comp settings - mode = self._get_comp_rendermode() - index = self.mode_options.findText(mode) - self.mode_options.setCurrentIndex(index) - - def _update_rendermode_info(self): - rendermode = self.mode_options.currentText() - self.mode_information.setText(_help[rendermode]) - - def _get_comp_name(self): - return self._comp.GetAttrs("COMPS_Name") - - def _get_comp_rendermode(self): - return self._comp.GetData("openpype.rendermode") or "local" - - def _set_comp_rendermode(self): - rendermode = self.mode_options.currentText() - self._comp.SetData("openpype.rendermode", rendermode) - - self._comp.Print("Updated render mode to '%s'\n" % rendermode) - self.hide() - - def _validation(self): - ui_mode = self.mode_options.currentText() - comp_mode = self._get_comp_rendermode() - - return comp_mode == ui_mode From 075ea125894262ad87597f46e78d3f54e8997d13 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Mon, 13 Mar 2023 18:01:33 +0100 Subject: [PATCH 056/228] Remove reference to set_rendermode (forgot to delete it in last push) --- openpype/hosts/fusion/api/menu.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/fusion/api/menu.py b/openpype/hosts/fusion/api/menu.py index ba3ce6798d..92f38a64c2 100644 --- a/openpype/hosts/fusion/api/menu.py +++ b/openpype/hosts/fusion/api/menu.py @@ -6,7 +6,6 @@ from openpype.tools.utils import host_tools from openpype.style import load_stylesheet from openpype.lib import register_event_callback from openpype.hosts.fusion.scripts import ( - set_rendermode, duplicate_with_inputs, ) from openpype.hosts.fusion.api.lib import ( From 10ff798ed59e72f006f4aac8df09a8bb119c647b Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Tue, 14 Mar 2023 17:18:56 +0100 Subject: [PATCH 057/228] Fixed "Select invalid" --- .../validate_local_frames_existence.py | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/validate_local_frames_existence.py b/openpype/hosts/fusion/plugins/publish/validate_local_frames_existence.py index d80e11e078..c1cba795e1 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_local_frames_existence.py +++ b/openpype/hosts/fusion/plugins/publish/validate_local_frames_existence.py @@ -8,7 +8,9 @@ from openpype.hosts.fusion.api.action import SelectInvalidAction class ValidateLocalFramesExistence(pyblish.api.InstancePlugin): - """Checks if files for savers that's set to publish existing frames exists""" + """Checks if files for savers that's set + to publish existing frames exists + """ order = pyblish.api.ValidatorOrder label = "Validate Existing Frames Exists" @@ -17,7 +19,7 @@ class ValidateLocalFramesExistence(pyblish.api.InstancePlugin): actions = [RepairAction, SelectInvalidAction] @classmethod - def get_invalid(cls, instance): + def get_invalid(cls, instance, non_existing_frames=[]): active = instance.data.get("active", instance.data.get("publish")) if not active: return [] @@ -37,29 +39,27 @@ class ValidateLocalFramesExistence(pyblish.api.InstancePlugin): for frame in range(frame_start, frame_end + 1) ] - non_existing_frames = [] - for file in files: - cls.log.error(file) if not os.path.exists(os.path.join(output_dir, file)): + cls.log.error( + f"Missing file: {os.path.join(output_dir, file)}" + ) non_existing_frames.append(file) if len(non_existing_frames) > 0: - cls.log.error( - "Some of {}'s files does not exist".format(tool.Name) - ) - return [tool, output_dir, non_existing_frames] + cls.log.error(f"Some of {tool.Name}'s files does not exist") + return [tool] def process(self, instance): - invalid = self.get_invalid(instance) + non_existing_frames = [] + invalid = self.get_invalid(instance, non_existing_frames) if invalid: raise PublishValidationError( "{} is set to publish existing frames but " - "some frames are missing in the folder:\n\n{}" + "some frames are missing. " "The missing file(s) are:\n\n{}".format( invalid[0].Name, - invalid[1], - "\n\n".join(invalid[2]), + "\n\n".join(non_existing_frames), ), title=self.label, ) From 9741d47fbc7c03d20cbc775f61ae19e321bb631c Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Tue, 14 Mar 2023 17:19:19 +0100 Subject: [PATCH 058/228] Added crude repair functionality --- .../publish/validate_local_frames_existence.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/validate_local_frames_existence.py b/openpype/hosts/fusion/plugins/publish/validate_local_frames_existence.py index c1cba795e1..4b50e0b837 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_local_frames_existence.py +++ b/openpype/hosts/fusion/plugins/publish/validate_local_frames_existence.py @@ -67,5 +67,14 @@ class ValidateLocalFramesExistence(pyblish.api.InstancePlugin): @classmethod def repair(cls, instance): invalid = cls.get_invalid(instance) - for tool in invalid: - tool.SetInput("CreateDir", 1.0) + if invalid: + data = invalid[0].GetData("openpype") + + # Change render target to local to render locally + data["creator_attributes"]["render_target"] = "local" + + invalid[0].SetData("openpype", data) + cls.log.error( + f"Reload the publisher and {invalid[0].Name} " + "will be set to render locally" + ) From 1e9670f54dd9121527438c25649c53016fd4ea92 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Tue, 14 Mar 2023 17:19:43 +0100 Subject: [PATCH 059/228] Fixed docstring and removed unused variables --- .../fusion/plugins/publish/collect_renders.py | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_renders.py b/openpype/hosts/fusion/plugins/publish/collect_renders.py index cdd37b3b65..be405d2495 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_renders.py +++ b/openpype/hosts/fusion/plugins/publish/collect_renders.py @@ -3,21 +3,11 @@ from pprint import pformat class CollectFusionRenders(pyblish.api.InstancePlugin): - """Collect current comp's render Mode + """Collect current saver node's render Mode Options: - local - farm - - Note that this value is set for each comp separately. When you save the - comp this information will be stored in that file. If for some reason the - available tool does not visualize which render mode is set for the - current comp, please run the following line in the console (Py2) - - comp.GetData("openpype.rendermode") - - This will return the name of the current render mode as seen above under - Options. + local (Render locally) + frames (Use existing frames) """ @@ -29,10 +19,8 @@ class CollectFusionRenders(pyblish.api.InstancePlugin): def process(self, instance): self.log.debug(pformat(instance.data)) - saver_node = instance.data["transientData"]["tool"] render_target = instance.data["render_target"] family = instance.data["family"] - families = instance.data["families"] # add targeted family to families instance.data["families"].append( From 191882424047cb517f0fc97171819dcca6d84093 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Tue, 14 Mar 2023 17:56:11 +0100 Subject: [PATCH 060/228] Moved the review representation back to render_local.py --- .../plugins/publish/extract_render_local.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/openpype/hosts/fusion/plugins/publish/extract_render_local.py b/openpype/hosts/fusion/plugins/publish/extract_render_local.py index 62625c899a..ea801107c1 100644 --- a/openpype/hosts/fusion/plugins/publish/extract_render_local.py +++ b/openpype/hosts/fusion/plugins/publish/extract_render_local.py @@ -29,6 +29,41 @@ class FusionRenderLocal( ) ) + # Generate the frame list + 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, + } + + # Get the colorspace represenation + 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) + def render_once(self, context): """Render context comp only once, even with more render instances""" From c7ae8b53201b5a0f223aec38cc2244b305581607 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Tue, 14 Mar 2023 17:57:45 +0100 Subject: [PATCH 061/228] Collect all frames for "using existing frames" savers --- ...iew_data.py => collect_existing_frames.py} | 20 ++++++------------- ... => validate_existing_frames_existence.py} | 0 2 files changed, 6 insertions(+), 14 deletions(-) rename openpype/hosts/fusion/plugins/publish/{extract_review_data.py => collect_existing_frames.py} (74%) rename openpype/hosts/fusion/plugins/publish/{validate_local_frames_existence.py => validate_existing_frames_existence.py} (100%) diff --git a/openpype/hosts/fusion/plugins/publish/extract_review_data.py b/openpype/hosts/fusion/plugins/publish/collect_existing_frames.py similarity index 74% rename from openpype/hosts/fusion/plugins/publish/extract_review_data.py rename to openpype/hosts/fusion/plugins/publish/collect_existing_frames.py index d9416771f6..7c5f7cd55d 100644 --- a/openpype/hosts/fusion/plugins/publish/extract_review_data.py +++ b/openpype/hosts/fusion/plugins/publish/collect_existing_frames.py @@ -1,22 +1,14 @@ -import os import pyblish.api -from openpype.pipeline.publish import ( - ColormanagedPyblishPluginMixin, -) +import os -class FusionExtractReviewData( - pyblish.api.InstancePlugin, ColormanagedPyblishPluginMixin -): - """ - Extract the result of savers by starting a comp render - This will run the local render of Fusion. - """ +class CollectFusionRenders(pyblish.api.InstancePlugin): + """Collect all frames needed to publish existing frames""" - order = pyblish.api.ExtractorOrder - 0.1 - label = "Extract Review Data" + order = pyblish.api.CollectorOrder + 0.5 + label = "Collect Existing Frames" hosts = ["fusion"] - families = ["review"] + families = ["render.frames"] def process(self, instance): context = instance.context diff --git a/openpype/hosts/fusion/plugins/publish/validate_local_frames_existence.py b/openpype/hosts/fusion/plugins/publish/validate_existing_frames_existence.py similarity index 100% rename from openpype/hosts/fusion/plugins/publish/validate_local_frames_existence.py rename to openpype/hosts/fusion/plugins/publish/validate_existing_frames_existence.py From 1754ef32672256b1661669d510c1c83e6fa5f403 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Tue, 14 Mar 2023 18:06:37 +0100 Subject: [PATCH 062/228] Fixed mutable data structure problem --- .../plugins/publish/validate_existing_frames_existence.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/publish/validate_existing_frames_existence.py b/openpype/hosts/fusion/plugins/publish/validate_existing_frames_existence.py index 4b50e0b837..a43d2c691a 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_existing_frames_existence.py +++ b/openpype/hosts/fusion/plugins/publish/validate_existing_frames_existence.py @@ -19,7 +19,10 @@ class ValidateLocalFramesExistence(pyblish.api.InstancePlugin): actions = [RepairAction, SelectInvalidAction] @classmethod - def get_invalid(cls, instance, non_existing_frames=[]): + def get_invalid(cls, instance, non_existing_frames=None): + if non_existing_frames is None: + non_existing_frames = [] + active = instance.data.get("active", instance.data.get("publish")) if not active: return [] From 72a0c017608d7947b1c4ac8ba08fc4b66b3bd2eb Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 17 Mar 2023 22:26:36 +0800 Subject: [PATCH 063/228] add callback for setting resolution referenced from DB record for Max --- openpype/hosts/max/api/lib.py | 64 ++++++++++++++++++++++++++++++ openpype/hosts/max/api/menu.py | 13 +++++- openpype/hosts/max/api/pipeline.py | 4 ++ 3 files changed, 80 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 4fb750d91b..6a7f63ca75 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -6,6 +6,13 @@ from pymxs import runtime as rt from typing import Union import contextlib +from openpype.client import ( + get_project +) +from openpype.pipeline import legacy_io + +from openpype.pipeline.context_tools import get_current_project_asset + JSON_PREFIX = "JSON::" @@ -157,6 +164,63 @@ def get_multipass_setting(project_setting=None): ["multipass"]) +def set_scene_resolution(width, height): + """Set the render resolution + + Args: + width(int): value of the width + height(int): value of the height + + Returns: + None + + """ + rt.renderWidth = width + rt.renderHeight = height + + +def reset_scene_resolution(): + """Apply the scene resolution from the project definition + + scene resolution can be overwritten by an asset if the asset.data contains + any information regarding scene resolution . + + Returns: + None + """ + project_name = legacy_io.active_project() + project_doc = get_project(project_name) + project_data = project_doc["data"] + asset_data = get_current_project_asset()["data"] + + # Set project resolution + width_key = "resolutionWidth" + height_key = "resolutionHeight" + proj_width_key = project_data.get(width_key, 1920) + proj_height_key = project_data.get(height_key, 1080) + + width = asset_data.get(width_key, proj_width_key) + height = asset_data.get(height_key, proj_height_key) + + set_scene_resolution(width, height) + + +def set_context_setting(): + """Apply the project settings from the project definition + + Settings can be overwritten by an asset if the asset.data contains + any information regarding those settings. + + Examples of settings: + frame range + resolution + + Returns: + None + """ + reset_scene_resolution() + + def get_max_version(): """ Args: diff --git a/openpype/hosts/max/api/menu.py b/openpype/hosts/max/api/menu.py index 5c273b49b4..1f18972394 100644 --- a/openpype/hosts/max/api/menu.py +++ b/openpype/hosts/max/api/menu.py @@ -4,7 +4,7 @@ from qtpy import QtWidgets, QtCore from pymxs import runtime as rt from openpype.tools.utils import host_tools - +from openpype.hosts.max.api import lib class OpenPypeMenu(object): """Object representing OpenPype menu. @@ -107,6 +107,13 @@ class OpenPypeMenu(object): workfiles_action = QtWidgets.QAction("Work Files...", openpype_menu) workfiles_action.triggered.connect(self.workfiles_callback) openpype_menu.addAction(workfiles_action) + + openpype_menu.addSeparator() + + res_action = QtWidgets.QAction("Set Resolution", openpype_menu) + res_action.triggered.connect(self.resolution_callback) + openpype_menu.addAction(res_action) + return openpype_menu def load_callback(self): @@ -128,3 +135,7 @@ class OpenPypeMenu(object): def workfiles_callback(self): """Callback to show Workfiles tool.""" host_tools.show_workfiles(parent=self.main_widget) + + def resolution_callback(self): + """Callback to reset scene resolution""" + return lib.reset_scene_resolution() diff --git a/openpype/hosts/max/api/pipeline.py b/openpype/hosts/max/api/pipeline.py index f8a7b8ea5c..b3c12f59bc 100644 --- a/openpype/hosts/max/api/pipeline.py +++ b/openpype/hosts/max/api/pipeline.py @@ -49,6 +49,10 @@ class MaxHost(HostBase, IWorkfileHost, ILoadHost, INewPublisher): self.menu = OpenPypeMenu() self._has_been_setup = True + def context_setting(): + return lib.set_context_setting() + rt.callbacks.addScript(rt.Name('systemPostNew'), + context_setting) def has_unsaved_changes(self): # TODO: how to get it from 3dsmax? From 543ec0d926e70f285dc008a3fc00623873c8c6d0 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 17 Mar 2023 22:28:48 +0800 Subject: [PATCH 064/228] cosmetic issue fix --- openpype/hosts/max/api/pipeline.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/max/api/pipeline.py b/openpype/hosts/max/api/pipeline.py index b3c12f59bc..dacc402318 100644 --- a/openpype/hosts/max/api/pipeline.py +++ b/openpype/hosts/max/api/pipeline.py @@ -49,6 +49,7 @@ class MaxHost(HostBase, IWorkfileHost, ILoadHost, INewPublisher): self.menu = OpenPypeMenu() self._has_been_setup = True + def context_setting(): return lib.set_context_setting() rt.callbacks.addScript(rt.Name('systemPostNew'), From ab4179d49a06185da3b852861c50e216ad226238 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 17 Mar 2023 16:51:36 +0000 Subject: [PATCH 065/228] Properly get and set model panel camera. --- .../maya/plugins/publish/extract_playblast.py | 23 ++++++------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 06d3ccb4a9..a652db2eb5 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -114,21 +114,12 @@ class ExtractPlayblast(publish.Extractor): # Image planes do not update the file sequence unless the active panel # is viewing through the camera. - model_panel = instance.context.data.get("model_panel") - if not model_panel: - model_panels = cmds.getPanel(type="modelPanel") - visible_panels = cmds.getPanel(visiblePanels=True) - model_panel = list( - set(visible_panels) - (set(visible_panels) - set(model_panels)) - )[0] - instance.context.data["model_panel"] = model_panel - - panel_camera = instance.context.data.get("panel_camera") - if not panel_camera: - panel_camera = capture.parse_view(model_panel)["camera"] - instance.context.data["panel_camera"] = panel_camera - - cmds.modelPanel(model_panel, edit=True, camera=preset["camera"]) + panel_camera = cmds.modelPanel( + instance.data["panel"], query=True, camera=True + ) + cmds.modelPanel( + instance.data["panel"], edit=True, camera=preset["camera"] + ) # Disable Pan/Zoom. pan_zoom = cmds.getAttr("{}.panZoomEnabled".format(preset["camera"])) @@ -188,7 +179,7 @@ class ExtractPlayblast(publish.Extractor): cmds.setAttr("{}.panZoomEnabled".format(preset["camera"]), pan_zoom) # Restore panel camera. - cmds.modelPanel(model_panel, edit=True, camera=panel_camera) + cmds.modelPanel(instance.data["panel"], edit=True, camera=panel_camera) self.log.debug("playblast path {}".format(path)) From 1dec179dc766d341a56b37baf3e07f6d092aca98 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Sat, 18 Mar 2023 19:29:20 +0100 Subject: [PATCH 066/228] Changed the name from existing to expected frames --- ...ollect_existing_frames.py => collect_expected_frames.py} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename openpype/hosts/fusion/plugins/publish/{collect_existing_frames.py => collect_expected_frames.py} (88%) diff --git a/openpype/hosts/fusion/plugins/publish/collect_existing_frames.py b/openpype/hosts/fusion/plugins/publish/collect_expected_frames.py similarity index 88% rename from openpype/hosts/fusion/plugins/publish/collect_existing_frames.py rename to openpype/hosts/fusion/plugins/publish/collect_expected_frames.py index 7c5f7cd55d..485ca0c9a4 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_existing_frames.py +++ b/openpype/hosts/fusion/plugins/publish/collect_expected_frames.py @@ -2,11 +2,11 @@ import pyblish.api import os -class CollectFusionRenders(pyblish.api.InstancePlugin): - """Collect all frames needed to publish existing frames""" +class CollectFusionExpectedFrames(pyblish.api.InstancePlugin): + """Collect all frames needed to publish expected frames""" order = pyblish.api.CollectorOrder + 0.5 - label = "Collect Existing Frames" + label = "Collect Expected Frames" hosts = ["fusion"] families = ["render.frames"] From aad3325e046de74696ba93bd8b2d592b423a3d8b Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Sat, 18 Mar 2023 19:29:32 +0100 Subject: [PATCH 067/228] Cleaned up code --- openpype/hosts/fusion/plugins/publish/collect_renders.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_renders.py b/openpype/hosts/fusion/plugins/publish/collect_renders.py index be405d2495..7f38e68447 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_renders.py +++ b/openpype/hosts/fusion/plugins/publish/collect_renders.py @@ -1,5 +1,4 @@ import pyblish.api -from pprint import pformat class CollectFusionRenders(pyblish.api.InstancePlugin): @@ -17,8 +16,6 @@ class CollectFusionRenders(pyblish.api.InstancePlugin): families = ["render"] def process(self, instance): - self.log.debug(pformat(instance.data)) - render_target = instance.data["render_target"] family = instance.data["family"] @@ -26,5 +23,3 @@ class CollectFusionRenders(pyblish.api.InstancePlugin): instance.data["families"].append( "{}.{}".format(family, render_target) ) - - self.log.debug(pformat(instance.data)) From f2e782368fe5a135c1d01f4ee6a85fe8840e0f94 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Sat, 18 Mar 2023 19:30:13 +0100 Subject: [PATCH 068/228] Changed to dot notation for clearer code --- ...nce.py => validate_expected_frames_existence.py} | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) rename openpype/hosts/fusion/plugins/publish/{validate_existing_frames_existence.py => validate_expected_frames_existence.py} (87%) diff --git a/openpype/hosts/fusion/plugins/publish/validate_existing_frames_existence.py b/openpype/hosts/fusion/plugins/publish/validate_expected_frames_existence.py similarity index 87% rename from openpype/hosts/fusion/plugins/publish/validate_existing_frames_existence.py rename to openpype/hosts/fusion/plugins/publish/validate_expected_frames_existence.py index a43d2c691a..e74eb82366 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_existing_frames_existence.py +++ b/openpype/hosts/fusion/plugins/publish/validate_expected_frames_existence.py @@ -9,11 +9,11 @@ from openpype.hosts.fusion.api.action import SelectInvalidAction class ValidateLocalFramesExistence(pyblish.api.InstancePlugin): """Checks if files for savers that's set - to publish existing frames exists + to publish expected frames exists """ order = pyblish.api.ValidatorOrder - label = "Validate Existing Frames Exists" + label = "Validate Expected Frames Exists" families = ["render"] hosts = ["fusion"] actions = [RepairAction, SelectInvalidAction] @@ -71,13 +71,12 @@ class ValidateLocalFramesExistence(pyblish.api.InstancePlugin): def repair(cls, instance): invalid = cls.get_invalid(instance) if invalid: - data = invalid[0].GetData("openpype") + tool = invalid[0] # Change render target to local to render locally - data["creator_attributes"]["render_target"] = "local" + tool.SetData("openpype.creator_attributes.render_target", "local") - invalid[0].SetData("openpype", data) - cls.log.error( - f"Reload the publisher and {invalid[0].Name} " + cls.log.info( + f"Reload the publisher and {tool.Name} " "will be set to render locally" ) From 0401f40ac941b0fba6f31c954aeb98da380d6301 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Sat, 18 Mar 2023 19:50:11 +0100 Subject: [PATCH 069/228] Collect frames in expected_frames instead of in redner_local --- .../publish/collect_expected_frames.py | 7 +++- .../plugins/publish/extract_render_local.py | 39 +------------------ 2 files changed, 6 insertions(+), 40 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_expected_frames.py b/openpype/hosts/fusion/plugins/publish/collect_expected_frames.py index 485ca0c9a4..0ba777629f 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_expected_frames.py +++ b/openpype/hosts/fusion/plugins/publish/collect_expected_frames.py @@ -1,14 +1,17 @@ import pyblish.api +from openpype.pipeline import publish import os -class CollectFusionExpectedFrames(pyblish.api.InstancePlugin): +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.frames"] + families = ["render"] def process(self, instance): context = instance.context diff --git a/openpype/hosts/fusion/plugins/publish/extract_render_local.py b/openpype/hosts/fusion/plugins/publish/extract_render_local.py index ea801107c1..673c5a3ce3 100644 --- a/openpype/hosts/fusion/plugins/publish/extract_render_local.py +++ b/openpype/hosts/fusion/plugins/publish/extract_render_local.py @@ -4,9 +4,7 @@ from openpype.pipeline import publish from openpype.hosts.fusion.api import comp_lock_and_undo_chunk -class FusionRenderLocal( - pyblish.api.InstancePlugin, publish.ColormanagedPyblishPluginMixin -): +class FusionRenderLocal(pyblish.api.InstancePlugin): """Render the current Fusion composition locally.""" order = pyblish.api.ExtractorOrder - 0.2 @@ -29,41 +27,6 @@ class FusionRenderLocal( ) ) - # Generate the frame list - 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, - } - - # Get the colorspace represenation - 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) - def render_once(self, context): """Render context comp only once, even with more render instances""" From 519cd3050888478202e7e2328fb3894ca5a7ab0a Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Sat, 18 Mar 2023 19:50:49 +0100 Subject: [PATCH 070/228] Removed code that should never be able to happen --- .../fusion/plugins/publish/validate_create_folder_checked.py | 4 ---- .../plugins/publish/validate_expected_frames_existence.py | 4 ---- 2 files changed, 8 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 53dea2af71..8a91f23578 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py +++ b/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py @@ -21,10 +21,6 @@ class ValidateCreateFolderChecked(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance): - active = instance.data.get("active", instance.data.get("publish")) - if not active: - return [] - tool = instance[0] create_dir = tool.GetInput("CreateDir") if create_dir == 0.0: 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 e74eb82366..c208b8ef15 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_expected_frames_existence.py +++ b/openpype/hosts/fusion/plugins/publish/validate_expected_frames_existence.py @@ -23,10 +23,6 @@ class ValidateLocalFramesExistence(pyblish.api.InstancePlugin): if non_existing_frames is None: non_existing_frames = [] - active = instance.data.get("active", instance.data.get("publish")) - if not active: - return [] - if instance.data.get("render_target") == "frames": tool = instance[0] From d24c4e03dd41faa775efdc57b31b3cc3132f10b8 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 20 Mar 2023 17:56:50 +0800 Subject: [PATCH 071/228] 3dsmax: render instance settings in creator --- .../hosts/max/plugins/create/create_render.py | 50 +++++++++++++++++++ .../max/plugins/publish/collect_render.py | 11 +++- .../deadline/plugins/publish/collect_pools.py | 2 +- .../plugins/publish/submit_max_deadline.py | 13 +++-- 4 files changed, 66 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index 269fff2e32..31e3ddcb09 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -1,8 +1,22 @@ # -*- coding: utf-8 -*- """Creator plugin for creating camera.""" from openpype.hosts.max.api import plugin +from openpype.lib import ( + TextDef, + BoolDef, + NumberDef, +) from openpype.pipeline import CreatedInstance from openpype.hosts.max.api.lib_rendersettings import RenderSettings +from openpype.settings import get_project_settings +from openpype.pipeline import legacy_io + + +def setting(project_setting=None): + render_setting = get_project_settings( + legacy_io.Session["AVALON_PROJECT"] + ) + return render_setting["deadline"]["publish"]["MaxSubmitDeadline"] class CreateRender(plugin.MaxCreator): @@ -31,3 +45,39 @@ 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_instance_attr_defs(self): + return [ + BoolDef("use_published", + default=setting()["active"], + label="Use Published Scene"), + + NumberDef("priority", + minimum=1, + maximum=250, + decimals=0, + default=setting()["priority"], + label="Priority"), + + NumberDef("chunkSize", + minimum=1, + maximum=50, + decimals=0, + default=setting()["chunk_size"], + label="Chunk Size"), + + TextDef("group", + default=setting()["group"], + label="Group Name"), + + TextDef("deadline_pool", + default=setting()["deadline_pool"], + label="Deadline Pool"), + + TextDef("deadline_pool_secondary", + default=setting()["deadline_pool_secondary"], + label="Deadline Pool Secondary") + ] + + def get_pre_create_attr_defs(self): + return self.get_instance_attr_defs() diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 7c9e311c2f..357135750f 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -47,11 +47,13 @@ class CollectRender(pyblish.api.InstancePlugin): self.log.debug(f"Setting {version_int} to context.") context.data["version"] = version_int + creator_attr = instance.data["creator_attributes"] + # setup the plugin as 3dsmax for the internal renderer data = { "subset": instance.name, "asset": asset, - "publish": True, + "publish": creator_attr["use_published"], "maxversion": str(get_max_version()), "imageFormat": img_format, "family": 'maxrender', @@ -61,7 +63,12 @@ class CollectRender(pyblish.api.InstancePlugin): "plugin": "3dsmax", "frameStart": context.data['frameStart'], "frameEnd": context.data['frameEnd'], - "version": version_int + "version": version_int, + "priority": creator_attr["priority"], + "chunkSize": creator_attr["chunkSize"], + "group": creator_attr["group"], + "primaryPool": creator_attr["deadline_pool"], + "secondaryPool": creator_attr["deadline_pool_secondary"] } self.log.info("data: {0}".format(data)) instance.data.update(data) diff --git a/openpype/modules/deadline/plugins/publish/collect_pools.py b/openpype/modules/deadline/plugins/publish/collect_pools.py index 48130848d5..c9b4f485d8 100644 --- a/openpype/modules/deadline/plugins/publish/collect_pools.py +++ b/openpype/modules/deadline/plugins/publish/collect_pools.py @@ -10,7 +10,7 @@ class CollectDeadlinePools(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder + 0.420 label = "Collect Deadline Pools" - families = ["rendering", "render.farm", "renderFarm", "renderlayer"] + families = ["rendering", "render.farm", "renderFarm", "renderlayer", "maxrender"] primary_pool = None secondary_pool = None diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index 417a03de74..6d62dd7f33 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -21,7 +21,7 @@ class MaxPluginInfo(object): SaveFile = attr.ib(default=True) IgnoreInputs = attr.ib(default=True) - +#TODO: add the optional attirbute class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): label = "Submit Render to Deadline" @@ -49,11 +49,13 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): instance = self._instance context = instance.context - # Always use the original work file name for the Job name even when # rendering is done from the published Work File. The original work # file name is clearer because it can also have subversion strings, # etc. which are stripped for the published file. + if not instance.data.get("publish"): + self.use_published = False + src_filepath = context.data["currentFile"] src_filename = os.path.basename(src_filepath) @@ -71,13 +73,10 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): job_info.Pool = instance.data.get("primaryPool") job_info.SecondaryPool = instance.data.get("secondaryPool") - job_info.ChunkSize = instance.data.get("chunkSize", 1) + job_info.ChunkSize = instance.data.get("chunkSize", self.chunk_size) job_info.Comment = context.data.get("comment") job_info.Priority = instance.data.get("priority", self.priority) - job_info.FramesPerTask = instance.data.get("framesPerTask", 1) - - if self.group: - job_info.Group = self.group + job_info.Group = instance.data.get("group", self.group) # Add options from RenderGlobals render_globals = instance.data.get("renderGlobals", {}) From fff1e3f0c1b257d03170fd665ccd09636fc611ad Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 20 Mar 2023 18:08:51 +0800 Subject: [PATCH 072/228] cosmetic fix and add optional for 3dsmax deadline submission --- .../deadline/plugins/publish/collect_pools.py | 6 +++++- .../deadline/plugins/publish/submit_max_deadline.py | 12 +++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/collect_pools.py b/openpype/modules/deadline/plugins/publish/collect_pools.py index c9b4f485d8..3a424f9e74 100644 --- a/openpype/modules/deadline/plugins/publish/collect_pools.py +++ b/openpype/modules/deadline/plugins/publish/collect_pools.py @@ -10,7 +10,11 @@ class CollectDeadlinePools(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder + 0.420 label = "Collect Deadline Pools" - families = ["rendering", "render.farm", "renderFarm", "renderlayer", "maxrender"] + families = ["rendering", + "render.farm", + "renderFarm", + "renderlayer", + "maxrender"] primary_pool = None secondary_pool = None diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index 6d62dd7f33..1f1a59a8e6 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -3,7 +3,10 @@ import getpass import copy import attr -from openpype.pipeline import legacy_io +from openpype.pipeline import ( + legacy_io, + OptionalPyblishPluginMixin +) from openpype.settings import get_project_settings from openpype.hosts.max.api.lib import ( get_current_renderer, @@ -21,8 +24,8 @@ class MaxPluginInfo(object): SaveFile = attr.ib(default=True) IgnoreInputs = attr.ib(default=True) -#TODO: add the optional attirbute -class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): +class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, + OptionalPyblishPluginMixin): label = "Submit Render to Deadline" hosts = ["max"] @@ -39,6 +42,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): deadline_pool = None deadline_pool_secondary = None framePerTask = 1 + optional = True def get_job_info(self): job_info = DeadlineJobInfo(Plugin="3dsmax") @@ -49,6 +53,8 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): instance = self._instance context = instance.context + if not self.is_active(instance.data): + return # Always use the original work file name for the Job name even when # rendering is done from the published Work File. The original work # file name is clearer because it can also have subversion strings, From 7baa5754e05d6f30794cb265245f8d55a68a0c37 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 20 Mar 2023 18:09:39 +0800 Subject: [PATCH 073/228] cosmetic issue fix --- openpype/modules/deadline/plugins/publish/submit_max_deadline.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index 1f1a59a8e6..d04c4b9c09 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -24,6 +24,7 @@ class MaxPluginInfo(object): SaveFile = attr.ib(default=True) IgnoreInputs = attr.ib(default=True) + class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, OptionalPyblishPluginMixin): From 507941c20898a9cf524851189a10176263543116 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 20 Mar 2023 18:36:36 +0800 Subject: [PATCH 074/228] use apply_settings --- .../hosts/max/plugins/create/create_render.py | 32 ++++++++++--------- .../plugins/publish/submit_max_deadline.py | 4 +-- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index 31e3ddcb09..dc1605bb5e 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -8,15 +8,6 @@ from openpype.lib import ( ) from openpype.pipeline import CreatedInstance from openpype.hosts.max.api.lib_rendersettings import RenderSettings -from openpype.settings import get_project_settings -from openpype.pipeline import legacy_io - - -def setting(project_setting=None): - render_setting = get_project_settings( - legacy_io.Session["AVALON_PROJECT"] - ) - return render_setting["deadline"]["publish"]["MaxSubmitDeadline"] class CreateRender(plugin.MaxCreator): @@ -25,6 +16,17 @@ class CreateRender(plugin.MaxCreator): family = "maxrender" icon = "gear" + def apply_settings(self, project_settings, system_settings): + plugin_settings = ( + project_settings["deadline"]["publish"]["MaxSubmitDeadline"] + ) + self.use_published = plugin_settings["use_published"] + self.priority = plugin_settings["priority"] + self.chunkSize = plugin_settings["chunk_size"] + self.group = plugin_settings["group"] + self.deadline_pool = plugin_settings["deadline_pool"] + self.deadline_pool_secondary = plugin_settings["deadline_pool_secondary"] + def create(self, subset_name, instance_data, pre_create_data): from pymxs import runtime as rt sel_obj = list(rt.selection) @@ -49,33 +51,33 @@ class CreateRender(plugin.MaxCreator): def get_instance_attr_defs(self): return [ BoolDef("use_published", - default=setting()["active"], + default=self.use_published, label="Use Published Scene"), NumberDef("priority", minimum=1, maximum=250, decimals=0, - default=setting()["priority"], + default=self.priority, label="Priority"), NumberDef("chunkSize", minimum=1, maximum=50, decimals=0, - default=setting()["chunk_size"], + default=self.chunkSize, label="Chunk Size"), TextDef("group", - default=setting()["group"], + default=self.group, label="Group Name"), TextDef("deadline_pool", - default=setting()["deadline_pool"], + default=self.deadline_pool, label="Deadline Pool"), TextDef("deadline_pool_secondary", - default=setting()["deadline_pool_secondary"], + default=self.deadline_pool_secondary, label="Deadline Pool Secondary") ] diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index d04c4b9c09..92e06ca765 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -60,8 +60,6 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, # rendering is done from the published Work File. The original work # file name is clearer because it can also have subversion strings, # etc. which are stripped for the published file. - if not instance.data.get("publish"): - self.use_published = False src_filepath = context.data["currentFile"] src_filename = os.path.basename(src_filepath) @@ -80,7 +78,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, job_info.Pool = instance.data.get("primaryPool") job_info.SecondaryPool = instance.data.get("secondaryPool") - job_info.ChunkSize = instance.data.get("chunkSize", self.chunk_size) + job_info.ChunkSize = instance.data.get("chunkSize", 1) job_info.Comment = context.data.get("comment") job_info.Priority = instance.data.get("priority", self.priority) job_info.Group = instance.data.get("group", self.group) From e00ef8210c9fb592bf9ee84c5c8e56df785a2845 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 20 Mar 2023 18:37:58 +0800 Subject: [PATCH 075/228] cosmetic issue fix --- openpype/hosts/max/plugins/create/create_render.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index dc1605bb5e..a24c5ea000 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -25,7 +25,7 @@ class CreateRender(plugin.MaxCreator): self.chunkSize = plugin_settings["chunk_size"] self.group = plugin_settings["group"] self.deadline_pool = plugin_settings["deadline_pool"] - self.deadline_pool_secondary = plugin_settings["deadline_pool_secondary"] + self.pool_secondary = plugin_settings["deadline_pool_secondary"] def create(self, subset_name, instance_data, pre_create_data): from pymxs import runtime as rt @@ -77,7 +77,7 @@ class CreateRender(plugin.MaxCreator): label="Deadline Pool"), TextDef("deadline_pool_secondary", - default=self.deadline_pool_secondary, + default=self.pool_secondary, label="Deadline Pool Secondary") ] From 36db7faab4c763ed58d9ac3a124a54c9ecac5e9b Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 20 Mar 2023 23:51:49 +0800 Subject: [PATCH 076/228] only allows user to set up the attributes in publish tab --- .../hosts/max/plugins/create/create_render.py | 52 ----------- .../max/plugins/publish/collect_render.py | 9 +- .../plugins/publish/submit_max_deadline.py | 86 ++++++++++++++++--- 3 files changed, 77 insertions(+), 70 deletions(-) diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index a24c5ea000..269fff2e32 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -1,11 +1,6 @@ # -*- coding: utf-8 -*- """Creator plugin for creating camera.""" from openpype.hosts.max.api import plugin -from openpype.lib import ( - TextDef, - BoolDef, - NumberDef, -) from openpype.pipeline import CreatedInstance from openpype.hosts.max.api.lib_rendersettings import RenderSettings @@ -16,17 +11,6 @@ class CreateRender(plugin.MaxCreator): family = "maxrender" icon = "gear" - def apply_settings(self, project_settings, system_settings): - plugin_settings = ( - project_settings["deadline"]["publish"]["MaxSubmitDeadline"] - ) - self.use_published = plugin_settings["use_published"] - self.priority = plugin_settings["priority"] - self.chunkSize = plugin_settings["chunk_size"] - self.group = plugin_settings["group"] - self.deadline_pool = plugin_settings["deadline_pool"] - self.pool_secondary = plugin_settings["deadline_pool_secondary"] - def create(self, subset_name, instance_data, pre_create_data): from pymxs import runtime as rt sel_obj = list(rt.selection) @@ -47,39 +31,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_instance_attr_defs(self): - return [ - BoolDef("use_published", - default=self.use_published, - label="Use Published Scene"), - - NumberDef("priority", - minimum=1, - maximum=250, - decimals=0, - default=self.priority, - label="Priority"), - - NumberDef("chunkSize", - minimum=1, - maximum=50, - decimals=0, - default=self.chunkSize, - label="Chunk Size"), - - TextDef("group", - default=self.group, - label="Group Name"), - - TextDef("deadline_pool", - default=self.deadline_pool, - label="Deadline Pool"), - - TextDef("deadline_pool_secondary", - default=self.pool_secondary, - label="Deadline Pool Secondary") - ] - - def get_pre_create_attr_defs(self): - return self.get_instance_attr_defs() diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 357135750f..63e4108c84 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -47,13 +47,11 @@ class CollectRender(pyblish.api.InstancePlugin): self.log.debug(f"Setting {version_int} to context.") context.data["version"] = version_int - creator_attr = instance.data["creator_attributes"] - # setup the plugin as 3dsmax for the internal renderer data = { "subset": instance.name, "asset": asset, - "publish": creator_attr["use_published"], + "publish": True, "maxversion": str(get_max_version()), "imageFormat": img_format, "family": 'maxrender', @@ -64,11 +62,6 @@ class CollectRender(pyblish.api.InstancePlugin): "frameStart": context.data['frameStart'], "frameEnd": context.data['frameEnd'], "version": version_int, - "priority": creator_attr["priority"], - "chunkSize": creator_attr["chunkSize"], - "group": creator_attr["group"], - "primaryPool": creator_attr["deadline_pool"], - "secondaryPool": creator_attr["deadline_pool_secondary"] } self.log.info("data: {0}".format(data)) instance.data.update(data) diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index 92e06ca765..83ecdfd6af 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -3,9 +3,14 @@ import getpass import copy import attr +from openpype.lib import ( + TextDef, + BoolDef, + NumberDef, +) from openpype.pipeline import ( legacy_io, - OptionalPyblishPluginMixin + OpenPypePyblishPluginMixin ) from openpype.settings import get_project_settings from openpype.hosts.max.api.lib import ( @@ -26,7 +31,7 @@ class MaxPluginInfo(object): class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, - OptionalPyblishPluginMixin): + OpenPypePyblishPluginMixin): label = "Submit Render to Deadline" hosts = ["max"] @@ -36,7 +41,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, use_published = True priority = 50 tile_priority = 50 - chunk_size = 1 + chunkSize = 1 jobInfo = {} pluginInfo = {} group = None @@ -45,6 +50,22 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, framePerTask = 1 optional = True + @classmethod + def apply_settings(cls, project_settings, system_settings): + settings = project_settings["deadline"]["publish"]["MaxSubmitDeadline"] # noqa + + # Take some defaults from settings + cls.use_published = settings.get("use_published", + cls.use_published) + cls.priority = settings.get("priority", + cls.priority) + cls.chunkSize = settings.get("chunk_size", cls.chunkSize) + cls.group = settings.get("group", cls.group) + cls.deadline_pool = settings.get("deadline_pool", + cls.deadline_pool) + cls.deadline_pool_secondary = settings.get("deadline_pool_secondary", + cls.deadline_pool_secondary) + def get_job_info(self): job_info = DeadlineJobInfo(Plugin="3dsmax") @@ -54,8 +75,6 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, instance = self._instance context = instance.context - if not self.is_active(instance.data): - return # Always use the original work file name for the Job name even when # rendering is done from the published Work File. The original work # file name is clearer because it can also have subversion strings, @@ -76,12 +95,22 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, ) job_info.Frames = frames - job_info.Pool = instance.data.get("primaryPool") - job_info.SecondaryPool = instance.data.get("secondaryPool") - job_info.ChunkSize = instance.data.get("chunkSize", 1) + attr_values = self.get_attr_values_from_data(instance.data) + + if attr_values.get("deadline_pool"): + job_info.Pool = attr_values.get("deadline_pool") + else: + job_info.Pool = instance.data.get("primaryPool") + if attr_values.get("deadline_pool_secondary"): + job_info.SecondaryPool = attr_values.get("deadline_pool_secondary") + else: + job_info.SecondaryPool = instance.data.get("secondaryPool", + self.deadline_pool_secondary) + + job_info.ChunkSize = attr_values.get("chunkSize", 1) job_info.Comment = context.data.get("comment") - job_info.Priority = instance.data.get("priority", self.priority) - job_info.Group = instance.data.get("group", self.group) + job_info.Priority = attr_values.get("priority", self.priority) + job_info.Group = attr_values.get("group", self.group) # Add options from RenderGlobals render_globals = instance.data.get("renderGlobals", {}) @@ -220,3 +249,40 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, plugin_info.update(plugin_data) return job_info, plugin_info + + @classmethod + def get_attribute_defs(cls): + defs = super(MaxSubmitDeadline, cls).get_attribute_defs() + defs.extend([ + BoolDef("use_published", + default=cls.use_published, + label="Use Published Scene"), + + NumberDef("priority", + minimum=1, + maximum=250, + decimals=0, + default=cls.priority, + label="Priority"), + + NumberDef("chunkSize", + minimum=1, + maximum=50, + decimals=0, + default=cls.chunkSize, + label="Frame Per Task"), + + TextDef("group", + default=cls.group, + label="Group Name"), + + TextDef("deadline_pool", + default=cls.deadline_pool, + label="Deadline Pool"), + + TextDef("deadline_pool_secondary", + default=cls.deadline_pool_secondary, + label="Deadline Pool Secondary") + ]) + + return defs From fb6bc696f8a5f94b54689c2d4e6c390f8058220c Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 20 Mar 2023 23:54:16 +0800 Subject: [PATCH 077/228] cosmetic issue 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 83ecdfd6af..478c2ce2de 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -105,7 +105,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, job_info.SecondaryPool = attr_values.get("deadline_pool_secondary") else: job_info.SecondaryPool = instance.data.get("secondaryPool", - self.deadline_pool_secondary) + self.deadline_pool_secondary) # noqa job_info.ChunkSize = attr_values.get("chunkSize", 1) job_info.Comment = context.data.get("comment") From 2af4c94e2ca6328328b49bed562c2a4106ba9c90 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 21 Mar 2023 15:43:45 +0800 Subject: [PATCH 078/228] move the pool settings into the pool collectors --- .../deadline/plugins/publish/collect_pools.py | 46 +++++++++++++++++-- .../plugins/publish/submit_max_deadline.py | 25 ++-------- .../publish/validate_deadline_pools.py | 6 ++- 3 files changed, 51 insertions(+), 26 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/collect_pools.py b/openpype/modules/deadline/plugins/publish/collect_pools.py index 3a424f9e74..eb84308e9d 100644 --- a/openpype/modules/deadline/plugins/publish/collect_pools.py +++ b/openpype/modules/deadline/plugins/publish/collect_pools.py @@ -3,9 +3,12 @@ """ import pyblish.api +from openpype.lib import TextDef +from openpype.pipeline.publish import OpenPypePyblishPluginMixin -class CollectDeadlinePools(pyblish.api.InstancePlugin): +class CollectDeadlinePools(pyblish.api.InstancePlugin, + OpenPypePyblishPluginMixin): """Collect pools from instance if present, from Setting otherwise.""" order = pyblish.api.CollectorOrder + 0.420 @@ -19,9 +22,46 @@ class CollectDeadlinePools(pyblish.api.InstancePlugin): primary_pool = None secondary_pool = None + @classmethod + def apply_settings(cls, project_settings, system_settings): + # deadline.publish.CollectDeadlinePools + settings = project_settings["deadline"]["publish"]["CollectDeadlinePools"] # noqa + cls.primary_pool = settings.get("primary_pool", None) + cls.secondary_pool = settings.get("secondary_pool", None) + for family in cls.families: + if family == "maxrender": + max_setting = project_settings["deadline"]["publish"]["MaxSubmitDeadline"] # noqa + cls.primary_pool = max_setting.get("deadline_pool", None) + cls.secondary_pool = max_setting.get("deadline_pool_secondary", None) + def process(self, instance): + + attr_values = self.get_attr_values_from_data(instance.data) if not instance.data.get("primaryPool"): - instance.data["primaryPool"] = self.primary_pool or "none" + instance.data["primaryPool"] = ( + attr_values.get("primaryPool") or self.primary_pool or "none" + ) if not instance.data.get("secondaryPool"): - instance.data["secondaryPool"] = self.secondary_pool or "none" + instance.data["secondaryPool"] = ( + attr_values.get("secondaryPool") or self.secondary_pool or "none" # noqa + ) + + @classmethod + def get_attribute_defs(cls): + # TODO: Preferably this would be an enum for the user + # but the Deadline server URL can be dynamic and + # can be set per render instance. Since get_attribute_defs + # can't be dynamic unfortunately EnumDef isn't possible (yet?) + # pool_names = self.deadline_module.get_deadline_pools(deadline_url, + # self.log) + # secondary_pool_names = ["-"] + pool_names + + return [ + TextDef("primaryPool", + label="Primary Pool", + default=cls.primary_pool), + TextDef("secondaryPool", + label="Secondary Pool", + default=cls.secondary_pool) + ] diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index 478c2ce2de..c55e85cfb0 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -61,10 +61,6 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, cls.priority) cls.chunkSize = settings.get("chunk_size", cls.chunkSize) cls.group = settings.get("group", cls.group) - cls.deadline_pool = settings.get("deadline_pool", - cls.deadline_pool) - cls.deadline_pool_secondary = settings.get("deadline_pool_secondary", - cls.deadline_pool_secondary) def get_job_info(self): job_info = DeadlineJobInfo(Plugin="3dsmax") @@ -95,17 +91,10 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, ) job_info.Frames = frames - attr_values = self.get_attr_values_from_data(instance.data) + job_info.Pool = instance.data.get("primaryPool") + job_info.SecondaryPool = instance.data.get("secondaryPool") - if attr_values.get("deadline_pool"): - job_info.Pool = attr_values.get("deadline_pool") - else: - job_info.Pool = instance.data.get("primaryPool") - if attr_values.get("deadline_pool_secondary"): - job_info.SecondaryPool = attr_values.get("deadline_pool_secondary") - else: - job_info.SecondaryPool = instance.data.get("secondaryPool", - self.deadline_pool_secondary) # noqa + attr_values = self.get_attr_values_from_data(instance.data) job_info.ChunkSize = attr_values.get("chunkSize", 1) job_info.Comment = context.data.get("comment") @@ -275,14 +264,6 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, TextDef("group", default=cls.group, label="Group Name"), - - TextDef("deadline_pool", - default=cls.deadline_pool, - label="Deadline Pool"), - - TextDef("deadline_pool_secondary", - default=cls.deadline_pool_secondary, - label="Deadline Pool Secondary") ]) return defs diff --git a/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py b/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py index 78eed17c98..3c02c7933f 100644 --- a/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py +++ b/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py @@ -17,7 +17,11 @@ class ValidateDeadlinePools(OptionalPyblishPluginMixin, label = "Validate Deadline Pools" order = pyblish.api.ValidatorOrder - families = ["rendering", "render.farm", "renderFarm", "renderlayer"] + families = ["rendering", + "render.farm", + "renderFarm", + "renderlayer", + "maxrender"] optional = True def process(self, instance): From c48fddbba33cdb290c6f86c34bc68bb25a943607 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 21 Mar 2023 15:45:01 +0800 Subject: [PATCH 079/228] cosmetic issue fix --- openpype/modules/deadline/plugins/publish/collect_pools.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/collect_pools.py b/openpype/modules/deadline/plugins/publish/collect_pools.py index eb84308e9d..b2732d375f 100644 --- a/openpype/modules/deadline/plugins/publish/collect_pools.py +++ b/openpype/modules/deadline/plugins/publish/collect_pools.py @@ -32,7 +32,8 @@ class CollectDeadlinePools(pyblish.api.InstancePlugin, if family == "maxrender": max_setting = project_settings["deadline"]["publish"]["MaxSubmitDeadline"] # noqa cls.primary_pool = max_setting.get("deadline_pool", None) - cls.secondary_pool = max_setting.get("deadline_pool_secondary", None) + cls.secondary_pool = max_setting.get("deadline_pool_secondary", + None) def process(self, instance): From d8441216e4fa4c8b1b0b1ee2e58d1df4eb2b28a3 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 21 Mar 2023 15:55:07 +0800 Subject: [PATCH 080/228] move the pool settings into the pool collectors --- openpype/modules/deadline/plugins/publish/collect_pools.py | 6 ------ .../modules/deadline/plugins/publish/submit_max_deadline.py | 6 ++++-- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/collect_pools.py b/openpype/modules/deadline/plugins/publish/collect_pools.py index b2732d375f..e221eb00ea 100644 --- a/openpype/modules/deadline/plugins/publish/collect_pools.py +++ b/openpype/modules/deadline/plugins/publish/collect_pools.py @@ -28,12 +28,6 @@ class CollectDeadlinePools(pyblish.api.InstancePlugin, settings = project_settings["deadline"]["publish"]["CollectDeadlinePools"] # noqa cls.primary_pool = settings.get("primary_pool", None) cls.secondary_pool = settings.get("secondary_pool", None) - for family in cls.families: - if family == "maxrender": - max_setting = project_settings["deadline"]["publish"]["MaxSubmitDeadline"] # noqa - cls.primary_pool = max_setting.get("deadline_pool", None) - cls.secondary_pool = max_setting.get("deadline_pool_secondary", - None) def process(self, instance): diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index c55e85cfb0..e681346556 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -91,8 +91,10 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, ) job_info.Frames = frames - job_info.Pool = instance.data.get("primaryPool") - job_info.SecondaryPool = instance.data.get("secondaryPool") + job_info.Pool = instance.data.get("primaryPool", + self.deadline_pool) + job_info.SecondaryPool = instance.data.get("secondaryPool", + self.deadline_pool_secondary) attr_values = self.get_attr_values_from_data(instance.data) From 6c1882236419d68934d6130242ffc9cf75c4eeba Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 21 Mar 2023 15:56:15 +0800 Subject: [PATCH 081/228] 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 e681346556..e99752b7ab 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -94,7 +94,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, job_info.Pool = instance.data.get("primaryPool", self.deadline_pool) job_info.SecondaryPool = instance.data.get("secondaryPool", - self.deadline_pool_secondary) + self.deadline_pool_secondary) # noqa attr_values = self.get_attr_values_from_data(instance.data) From 6bdca1561bc14fd024f0d264887f4a2202fbcc79 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 21 Mar 2023 16:00:29 +0800 Subject: [PATCH 082/228] clean up the duplicate attributes for pool settings --- .../deadline/plugins/publish/submit_max_deadline.py | 8 ++------ .../settings/defaults/project_settings/deadline.json | 2 -- .../projects_schema/schema_project_deadline.json | 10 ---------- 3 files changed, 2 insertions(+), 18 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index e99752b7ab..65d08b9ef4 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -45,8 +45,6 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, jobInfo = {} pluginInfo = {} group = None - deadline_pool = None - deadline_pool_secondary = None framePerTask = 1 optional = True @@ -91,10 +89,8 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, ) job_info.Frames = frames - job_info.Pool = instance.data.get("primaryPool", - self.deadline_pool) - job_info.SecondaryPool = instance.data.get("secondaryPool", - self.deadline_pool_secondary) # noqa + job_info.Pool = instance.data.get("primaryPool") + job_info.SecondaryPool = instance.data.get("secondaryPool") attr_values = self.get_attr_values_from_data(instance.data) diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json index 0cbd323299..dec6a405a0 100644 --- a/openpype/settings/defaults/project_settings/deadline.json +++ b/openpype/settings/defaults/project_settings/deadline.json @@ -44,8 +44,6 @@ "priority": 50, "chunk_size": 10, "group": "none", - "deadline_pool": "", - "deadline_pool_secondary": "", "framePerTask": 1 }, "NukeSubmitDeadline": { 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 9906939cc7..29551cc9f8 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json @@ -246,16 +246,6 @@ "key": "group", "label": "Group Name" }, - { - "type": "text", - "key": "deadline_pool", - "label": "Deadline pool" - }, - { - "type": "text", - "key": "deadline_pool_secondary", - "label": "Deadline pool (secondary)" - }, { "type": "number", "key": "framePerTask", From 44120036bf8d8c45fd89cae46837cbe8088c63ed Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 21 Mar 2023 16:03:40 +0800 Subject: [PATCH 083/228] clean up setting --- openpype/settings/defaults/project_settings/deadline.json | 3 +-- .../schemas/projects_schema/schema_project_deadline.json | 7 +------ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json index dec6a405a0..fdd70f1a44 100644 --- a/openpype/settings/defaults/project_settings/deadline.json +++ b/openpype/settings/defaults/project_settings/deadline.json @@ -43,8 +43,7 @@ "use_published": true, "priority": 50, "chunk_size": 10, - "group": "none", - "framePerTask": 1 + "group": "none" }, "NukeSubmitDeadline": { "enabled": true, 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 29551cc9f8..d8b5e4dc1f 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json @@ -239,17 +239,12 @@ { "type": "number", "key": "chunk_size", - "label": "Chunk Size" + "label": "Frame per Task" }, { "type": "text", "key": "group", "label": "Group Name" - }, - { - "type": "number", - "key": "framePerTask", - "label": "Frame Per Task" } ] }, From 828f59bfb55e10c2c207c98eab7d6f1184d2826e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 21 Mar 2023 16:37:58 +0800 Subject: [PATCH 084/228] clean up attributes --- .../modules/deadline/plugins/publish/submit_max_deadline.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index 65d08b9ef4..dec4bcc500 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -40,13 +40,10 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, use_published = True priority = 50 - tile_priority = 50 - chunkSize = 1 + chunk_size = 1 jobInfo = {} pluginInfo = {} group = None - framePerTask = 1 - optional = True @classmethod def apply_settings(cls, project_settings, system_settings): From 55b984dc66c4948b793033afe27e111047b62ae1 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 21 Mar 2023 17:45:35 +0800 Subject: [PATCH 085/228] scene length setting for 3dsmax --- openpype/hosts/max/api/lib.py | 127 +++++++++++++++++++++++++++++ openpype/hosts/max/api/menu.py | 21 ++++- openpype/hosts/max/api/pipeline.py | 5 ++ 3 files changed, 152 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 4fb750d91b..f1d1f91dd1 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -6,6 +6,14 @@ from pymxs import runtime as rt from typing import Union import contextlib +from openpype.client import ( + get_project, + get_asset_by_name +) +from openpype.pipeline import legacy_io + +from openpype.pipeline.context_tools import get_current_project_asset + JSON_PREFIX = "JSON::" @@ -157,6 +165,125 @@ def get_multipass_setting(project_setting=None): ["multipass"]) +def set_scene_resolution(width, height): + """Set the render resolution + + Args: + width(int): value of the width + height(int): value of the height + + Returns: + None + + """ + rt.renderWidth = width + rt.renderHeight = height + + +def reset_scene_resolution(): + """Apply the scene resolution from the project definition + + scene resolution can be overwritten by an asset if the asset.data contains + any information regarding scene resolution . + + Returns: + None + """ + project_name = legacy_io.active_project() + project_doc = get_project(project_name) + project_data = project_doc["data"] + asset_data = get_current_project_asset()["data"] + + # Set project resolution + width_key = "resolutionWidth" + height_key = "resolutionHeight" + proj_width_key = project_data.get(width_key, 1920) + proj_height_key = project_data.get(height_key, 1080) + + width = asset_data.get(width_key, proj_width_key) + height = asset_data.get(height_key, proj_height_key) + + set_scene_resolution(width, height) + + +def get_frame_range(): + """Get the current assets frame range and handles.""" + # Set frame start/end + project_name = legacy_io.active_project() + asset_name = legacy_io.Session["AVALON_ASSET"] + asset = get_asset_by_name(project_name, asset_name) + + frame_start = asset["data"].get("frameStart") + frame_end = asset["data"].get("frameEnd") + # Backwards compatibility + if frame_start is None or frame_end is None: + frame_start = asset["data"].get("edit_in") + frame_end = asset["data"].get("edit_out") + + if frame_start is None or frame_end is None: + return + + handles = asset["data"].get("handles") or 0 + handle_start = asset["data"].get("handleStart") + if handle_start is None: + handle_start = handles + + handle_end = asset["data"].get("handleEnd") + if handle_end is None: + handle_end = handles + + return { + "frameStart": frame_start, + "frameEnd": frame_end, + "handleStart": handle_start, + "handleEnd": handle_end + } + + +def reset_frame_range(fps=True): + """Set frame range to current asset + + Args: + animationRange: A System Global variable which lets you get and + set an Interval value that defines the start and end frames + of the Active Time Segment. + frameRate: A System Global variable which lets you get + and set an Integer value that defines the current + scene frame rate in frames-per-second. + """ + if fps: + fps_number = float(legacy_io.Session.get("AVALON_FPS", + 25)) + rt.frameRate = fps_number + + frame_range = get_frame_range() + + frame_start = frame_range["frameStart"] - int(frame_range["handleStart"]) + frame_end = frame_range["frameEnd"] + int(frame_range["handleEnd"]) + + frange_cmd = f"animationRange = interval {frame_start} {frame_end}" + + rt.execute(frange_cmd) + + +def set_context_setting(): + """Apply the project settings from the project definition + + Settings can be overwritten by an asset if the asset.data contains + any information regarding those settings. + + Examples of settings: + frame range + resolution + + Returns: + None + """ + reset_scene_resolution() + + reset_frame_range() + + def get_max_version(): """ Args: diff --git a/openpype/hosts/max/api/menu.py b/openpype/hosts/max/api/menu.py index 5c273b49b4..fdac5eba09 100644 --- a/openpype/hosts/max/api/menu.py +++ b/openpype/hosts/max/api/menu.py @@ -4,7 +4,7 @@ from qtpy import QtWidgets, QtCore from pymxs import runtime as rt from openpype.tools.utils import host_tools - +from openpype.hosts.max.api import lib class OpenPypeMenu(object): """Object representing OpenPype menu. @@ -107,6 +107,17 @@ class OpenPypeMenu(object): workfiles_action = QtWidgets.QAction("Work Files...", openpype_menu) workfiles_action.triggered.connect(self.workfiles_callback) openpype_menu.addAction(workfiles_action) + + openpype_menu.addSeparator() + + res_action = QtWidgets.QAction("Set Resolution", openpype_menu) + res_action.triggered.connect(self.resolution_callback) + openpype_menu.addAction(res_action) + + frame_action = QtWidgets.QAction("Set Frame Range", openpype_menu) + frame_action.triggered.connect(self.frame_range_callback) + openpype_menu.addAction(frame_action) + return openpype_menu def load_callback(self): @@ -128,3 +139,11 @@ class OpenPypeMenu(object): def workfiles_callback(self): """Callback to show Workfiles tool.""" host_tools.show_workfiles(parent=self.main_widget) + + def resolution_callback(self): + """Callback to reset scene resolution""" + return lib.reset_scene_resolution() + + def frame_range_callback(self): + """Callback to reset frame range""" + return lib.reset_frame_range() diff --git a/openpype/hosts/max/api/pipeline.py b/openpype/hosts/max/api/pipeline.py index f8a7b8ea5c..dacc402318 100644 --- a/openpype/hosts/max/api/pipeline.py +++ b/openpype/hosts/max/api/pipeline.py @@ -50,6 +50,11 @@ class MaxHost(HostBase, IWorkfileHost, ILoadHost, INewPublisher): self._has_been_setup = True + def context_setting(): + return lib.set_context_setting() + rt.callbacks.addScript(rt.Name('systemPostNew'), + context_setting) + def has_unsaved_changes(self): # TODO: how to get it from 3dsmax? return True From a24dcd207cceb8959a832a3c576a59bba0200064 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 21 Mar 2023 18:53:12 +0800 Subject: [PATCH 086/228] renaming chunksize variablesd --- .../modules/deadline/plugins/publish/submit_max_deadline.py | 4 ++-- 1 file changed, 2 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 dec4bcc500..c728b6b9c7 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -54,7 +54,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, cls.use_published) cls.priority = settings.get("priority", cls.priority) - cls.chunkSize = settings.get("chunk_size", cls.chunkSize) + cls.chuck_size = settings.get("chunk_size", cls.chunk_size) cls.group = settings.get("group", cls.group) def get_job_info(self): @@ -253,7 +253,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, minimum=1, maximum=50, decimals=0, - default=cls.chunkSize, + default=cls.chunk_size, label="Frame Per Task"), TextDef("group", From ba6135f49e7b0e1c8794f681b58a887e39a368e5 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 21 Mar 2023 14:05:00 +0100 Subject: [PATCH 087/228] Nuke: removing frame-range data --- .../nuke/plugins/publish/collect_backdrop.py | 32 ++----------------- 1 file changed, 2 insertions(+), 30 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/collect_backdrop.py b/openpype/hosts/nuke/plugins/publish/collect_backdrop.py index 8eaefa6854..7d51af7e9e 100644 --- a/openpype/hosts/nuke/plugins/publish/collect_backdrop.py +++ b/openpype/hosts/nuke/plugins/publish/collect_backdrop.py @@ -51,38 +51,10 @@ class CollectBackdrops(pyblish.api.InstancePlugin): instance.data["label"] = "{0} ({1} nodes)".format( bckn.name(), len(instance.data["transientData"]["childNodes"])) - instance.data["families"].append(instance.data["family"]) - - # Get frame range - handle_start = instance.context.data["handleStart"] - handle_end = instance.context.data["handleEnd"] - first_frame = int(nuke.root()["first_frame"].getValue()) - last_frame = int(nuke.root()["last_frame"].getValue()) - # get version version = instance.context.data.get('version') - if not version: - raise RuntimeError("Script name has no version in the name.") + if version: + instance.data['version'] = version - instance.data['version'] = version - - # Add version data to instance - version_data = { - "handles": handle_start, - "handleStart": handle_start, - "handleEnd": handle_end, - "frameStart": first_frame + handle_start, - "frameEnd": last_frame - handle_end, - "version": int(version), - "families": [instance.data["family"]] + instance.data["families"], - "subset": instance.data["subset"], - "fps": instance.context.data["fps"] - } - - instance.data.update({ - "versionData": version_data, - "frameStart": first_frame, - "frameEnd": last_frame - }) self.log.info("Backdrop instance collected: `{}`".format(instance)) From c42ce4e96c7d14767d1aa0a62b126ffa43ccd942 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Wed, 1 Mar 2023 14:41:51 +0100 Subject: [PATCH 088/228] Enhancement: Adding family requirements option to kitsu note --- .../plugins/publish/integrate_kitsu_note.py | 51 +++++++++++++++++++ .../defaults/project_settings/kitsu.json | 1 + .../projects_schema/schema_project_kitsu.json | 26 ++++++++++ 3 files changed, 78 insertions(+) diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py index 6fda32d85f..8e0037fcf7 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py @@ -15,6 +15,7 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): set_status_note = False note_status_shortname = "wfa" status_conditions = list() + family_requirements = list() # comment settings custom_comment_template = { @@ -72,6 +73,56 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): if self.set_status_note and allow_status_change: kitsu_status = gazu.task.get_task_status_by_short_name( self.note_status_shortname + families = set() + for instance in context: + if instance.data.get('publish'): + families.add(instance.data['family']) + + # Get note status, by default uses the task status for the note + # if it is not specified in the configuration + kitsu_task = context.data["kitsu_task"] + shortname = kitsu_task["task_status"]["short_name"].upper() + note_status = kitsu_task["task_status_id"] + if self.set_status_note and next( + ( + False + for status_except in self.status_exceptions + if shortname == status_except["short_name"].upper() + and status_except["condition"] == "equal" + or + shortname != status_except["short_name"].upper() + and status_except["condition"] == "not_equal" + ), + True, + ) and next( + ( + True + for family in families + if next( + ( + False + for family_req in self.family_requirements + if family_req['equality'] != 'equal' + and family_req['family'] == family + or + family_req['equality'] == 'not_equal' + and family_req['family'] == family + ), + True, + ) + ), + False, + ): + kitsu_status = gazu.task.get_task_status_by_short_name( + self.note_status_shortname + ) + if kitsu_status: + note_status = kitsu_status + self.log.info("Note Kitsu status: {}".format(note_status)) + else: + self.log.info( + "Cannot find {} status. The status will not be " + "changed!".format(self.note_status_shortname) ) if kitsu_status: note_status = kitsu_status diff --git a/openpype/settings/defaults/project_settings/kitsu.json b/openpype/settings/defaults/project_settings/kitsu.json index 0638450595..32c6c253c7 100644 --- a/openpype/settings/defaults/project_settings/kitsu.json +++ b/openpype/settings/defaults/project_settings/kitsu.json @@ -9,6 +9,7 @@ "set_status_note": false, "note_status_shortname": "wfa", "status_conditions": [], + "family_requirements": [], "custom_comment_template": { "enabled": false, "comment_template": "{comment}\n\n| | |\n|--|--|\n| version| `{version}` |\n| family | `{family}` |\n| name | `{name}` |" diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json b/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json index ee309f63a7..c32a2cb5af 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json @@ -79,6 +79,31 @@ }, "label": "Status shortname" }, + { + "type": "list", + "key": "family_requirements", + "label": "Family requirements", + "object_type": { + "type": "dict", + "key": "requirement_dict", + "children": [ + { + "type": "enum", + "key": "equality", + "label": "Equality", + "enum_items": [ + {"equal": "Equal"}, + {"not_equal": "Not equal"} + ] + }, + { + "type": "text", + "key": "family", + "label": "Family" + } + ] + } + }, { "type": "dict", "collapsible": true, @@ -102,6 +127,7 @@ "label": "Custom comment" } ] + } } ] } From b467909fd65622f524ff1ba35f2cca53cd62afab Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Wed, 1 Mar 2023 14:48:37 +0100 Subject: [PATCH 089/228] Using lower cases for families and a small fix --- .../modules/kitsu/plugins/publish/integrate_kitsu_note.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py index 8e0037fcf7..bd43fe3b7c 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py @@ -102,11 +102,11 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): ( False for family_req in self.family_requirements - if family_req['equality'] != 'equal' - and family_req['family'] == family + if family_req['equality'] == 'equal' + and family_req['family'].lower() != family or family_req['equality'] == 'not_equal' - and family_req['family'] == family + and family_req['family'].lower() == family ), True, ) From d3330f482a7e4d8945d871e13359f03938577240 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Wed, 1 Mar 2023 15:11:56 +0100 Subject: [PATCH 090/228] Edited kitsu module readme --- website/docs/module_kitsu.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/website/docs/module_kitsu.md b/website/docs/module_kitsu.md index 23898fba2e..08b7073c3d 100644 --- a/website/docs/module_kitsu.md +++ b/website/docs/module_kitsu.md @@ -43,10 +43,11 @@ Task status can be automatically set during publish thanks to `Integrate Kitsu N `Admin -> Studio Settings -> Project Settings -> Kitsu -> Integrate Kitsu Note`. -There are three settings available: +There are four settings available: - `Set status on note` -> turns on and off this integrator. - `Note shortname` -> Which status shortname should be set automatically (Case sensitive). - `Status conditions` -> Conditions that need to be met for kitsu status to be changed. You can add as many conditions as you like. There are two fields to each conditions: `Condition` (Whether current status should be equal or not equal to the condition status) and `Short name` (Kitsu Shortname of the condition status). +- `Family requirements` -> With this option you can add requirements to which families must be pushed or not in order to have the task status set by this integrator. There are two fields for each requirements: `Equality` (Same as the above) and `Family` (name of the family concerned by this requirement). For instance, adding one item set to `Not equal` and `workfile`, would mean the task status would change if a subset from another family than workfile is published (workfile can still be included), but not if you only publish the workfile subset. ![Integrate Kitsu Note project settings](assets/integrate_kitsu_note_settings.png) From 7356887df6402f5d4acfb7448a47aec5b5c33877 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Wed, 1 Mar 2023 18:45:15 +0100 Subject: [PATCH 091/228] Renaming equality into condition, made a new screenshot so both equal and not equal settings are shown --- .../plugins/publish/integrate_kitsu_note.py | 4 ++-- .../projects_schema/schema_project_kitsu.json | 4 ++-- .../assets/integrate_kitsu_note_settings.png | Bin 30524 -> 44847 bytes website/docs/module_kitsu.md | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py index bd43fe3b7c..2c85a5f70b 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py @@ -102,10 +102,10 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): ( False for family_req in self.family_requirements - if family_req['equality'] == 'equal' + if family_req['condition'] == 'equal' and family_req['family'].lower() != family or - family_req['equality'] == 'not_equal' + family_req['condition'] == 'not_equal' and family_req['family'].lower() == family ), True, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json b/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json index c32a2cb5af..ba77f2b471 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json @@ -89,8 +89,8 @@ "children": [ { "type": "enum", - "key": "equality", - "label": "Equality", + "key": "condition", + "label": "Condition", "enum_items": [ {"equal": "Equal"}, {"not_equal": "Not equal"} diff --git a/website/docs/assets/integrate_kitsu_note_settings.png b/website/docs/assets/integrate_kitsu_note_settings.png index 127e79ab8063dae0408f90a82d6bb4b5e233ed47..fd055942d023e2ffc12fc508cd7079eb834045c3 100644 GIT binary patch literal 44847 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 BT7Uom literal 30524 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 diff --git a/website/docs/module_kitsu.md b/website/docs/module_kitsu.md index 08b7073c3d..daa7102b79 100644 --- a/website/docs/module_kitsu.md +++ b/website/docs/module_kitsu.md @@ -47,7 +47,7 @@ There are four settings available: - `Set status on note` -> turns on and off this integrator. - `Note shortname` -> Which status shortname should be set automatically (Case sensitive). - `Status conditions` -> Conditions that need to be met for kitsu status to be changed. You can add as many conditions as you like. There are two fields to each conditions: `Condition` (Whether current status should be equal or not equal to the condition status) and `Short name` (Kitsu Shortname of the condition status). -- `Family requirements` -> With this option you can add requirements to which families must be pushed or not in order to have the task status set by this integrator. There are two fields for each requirements: `Equality` (Same as the above) and `Family` (name of the family concerned by this requirement). For instance, adding one item set to `Not equal` and `workfile`, would mean the task status would change if a subset from another family than workfile is published (workfile can still be included), but not if you only publish the workfile subset. +- `Family requirements` -> With this option you can add requirements to which families must be pushed or not in order to have the task status set by this integrator. There are two fields for each requirements: `Condition` (Same as the above) and `Family` (name of the family concerned by this requirement). For instance, adding one item set to `Not equal` and `workfile`, would mean the task status would change if a subset from another family than workfile is published (workfile can still be included), but not if you only publish the workfile subset. ![Integrate Kitsu Note project settings](assets/integrate_kitsu_note_settings.png) From d8360b5b19cad2110efdc71a9c72d007c2c8b927 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Wed, 8 Mar 2023 12:11:09 +0100 Subject: [PATCH 092/228] Made changes to resolve conflicts with develop. Changes how publisshed families are accessed. --- .../plugins/publish/integrate_kitsu_note.py | 96 +++++++++---------- 1 file changed, 47 insertions(+), 49 deletions(-) diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py index 2c85a5f70b..3c3607a987 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py @@ -73,56 +73,54 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): if self.set_status_note and allow_status_change: kitsu_status = gazu.task.get_task_status_by_short_name( self.note_status_shortname - families = set() - for instance in context: - if instance.data.get('publish'): - families.add(instance.data['family']) + families = set( + instance.data.get('kitsu_task') + for instance in context + if instance.data.get('publish') + ) - # Get note status, by default uses the task status for the note - # if it is not specified in the configuration - kitsu_task = context.data["kitsu_task"] - shortname = kitsu_task["task_status"]["short_name"].upper() - note_status = kitsu_task["task_status_id"] - if self.set_status_note and next( - ( - False - for status_except in self.status_exceptions - if shortname == status_except["short_name"].upper() - and status_except["condition"] == "equal" - or - shortname != status_except["short_name"].upper() - and status_except["condition"] == "not_equal" - ), - True, - ) and next( - ( - True - for family in families - if next( - ( - False - for family_req in self.family_requirements - if family_req['condition'] == 'equal' - and family_req['family'].lower() != family - or - family_req['condition'] == 'not_equal' - and family_req['family'].lower() == family - ), - True, - ) - ), - False, - ): - kitsu_status = gazu.task.get_task_status_by_short_name( - self.note_status_shortname - ) - if kitsu_status: - note_status = kitsu_status - self.log.info("Note Kitsu status: {}".format(note_status)) - else: - self.log.info( - "Cannot find {} status. The status will not be " - "changed!".format(self.note_status_shortname) + for instance in context: + kitsu_task = instance.data.get("kitsu_task") + if not kitsu_task: + continue + + # Get note status, by default uses the task status for the note + # if it is not specified in the configuration + kitsu_task = context.data["kitsu_task"] + shortname = kitsu_task["task_status"]["short_name"].upper() + note_status = kitsu_task["task_status_id"] + if self.set_status_note and next( + ( + False + for status_except in self.status_exceptions + if shortname == status_except["short_name"].upper() + and status_except["condition"] == "equal" + or + shortname != status_except["short_name"].upper() + and status_except["condition"] == "not_equal" + ), + True, + ) and next( + ( + True + for family in families + if next( + ( + False + for family_req in self.family_requirements + if family_req['condition'] == 'equal' + and family_req['family'].lower() != family + or + family_req['condition'] == 'not_equal' + and family_req['family'].lower() == family + ), + True, + ) + ), + False, + ): + kitsu_status = gazu.task.get_task_status_by_short_name( + self.note_status_shortname ) if kitsu_status: note_status = kitsu_status From 6e13275054fc3aaedb3f5d487ef45330aee615b9 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 21 Mar 2023 16:29:46 +0100 Subject: [PATCH 093/228] Cleanup unused imports --- openpype/hosts/fusion/plugins/publish/extract_render_local.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/extract_render_local.py b/openpype/hosts/fusion/plugins/publish/extract_render_local.py index 673c5a3ce3..16a582032f 100644 --- a/openpype/hosts/fusion/plugins/publish/extract_render_local.py +++ b/openpype/hosts/fusion/plugins/publish/extract_render_local.py @@ -1,6 +1,4 @@ -import os import pyblish.api -from openpype.pipeline import publish from openpype.hosts.fusion.api import comp_lock_and_undo_chunk From 83aabf4c2bfed63cbaa63fab3de14d5b53668a14 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Wed, 8 Mar 2023 12:15:00 +0100 Subject: [PATCH 094/228] Linted --- .../plugins/publish/integrate_kitsu_note.py | 71 +++++++++++-------- 1 file changed, 40 insertions(+), 31 deletions(-) diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py index 3c3607a987..f394a3d04b 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py @@ -55,6 +55,10 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): kitsu_task = instance.data.get("kitsu_task") if kitsu_task is None: continue + # Get comment text body + publish_comment = context.data.get("comment") + if not publish_comment: + self.log.info("Comment is not set.") # Get note status, by default uses the task status for the note # if it is not specified in the configuration @@ -74,9 +78,9 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): kitsu_status = gazu.task.get_task_status_by_short_name( self.note_status_shortname families = set( - instance.data.get('kitsu_task') + instance.data.get("kitsu_task") for instance in context - if instance.data.get('publish') + if instance.data.get("publish") ) for instance in context: @@ -89,35 +93,37 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): kitsu_task = context.data["kitsu_task"] shortname = kitsu_task["task_status"]["short_name"].upper() note_status = kitsu_task["task_status_id"] - if self.set_status_note and next( - ( - False - for status_except in self.status_exceptions - if shortname == status_except["short_name"].upper() - and status_except["condition"] == "equal" - or - shortname != status_except["short_name"].upper() - and status_except["condition"] == "not_equal" - ), - True, - ) and next( - ( - True - for family in families - if next( - ( - False - for family_req in self.family_requirements - if family_req['condition'] == 'equal' - and family_req['family'].lower() != family - or - family_req['condition'] == 'not_equal' - and family_req['family'].lower() == family - ), - True, - ) - ), - False, + if ( + self.set_status_note + and next( + ( + False + for status_except in self.status_exceptions + if shortname == status_except["short_name"].upper() + and status_except["condition"] == "equal" + or shortname != status_except["short_name"].upper() + and status_except["condition"] == "not_equal" + ), + True, + ) + and next( + ( + True + for family in families + if next( + ( + False + for family_req in self.family_requirements + if family_req["condition"] == "equal" + and family_req["family"].lower() != family + or family_req["condition"] == "not_equal" + and family_req["family"].lower() == family + ), + True, + ) + ), + False, + ) ): kitsu_status = gazu.task.get_task_status_by_short_name( self.note_status_shortname @@ -147,6 +153,9 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): ) kitsu_comment = gazu.task.add_comment( kitsu_task, note_status, comment=publish_comment + instance.data["kitsu_task"], + note_status, + comment=publish_comment, ) instance.data["kitsu_comment"] = kitsu_comment From 2022a38c650bf6dcd5603c88ecbebd09d176b2b0 Mon Sep 17 00:00:00 2001 From: Sharkitty <81646000+Sharkitty@users.noreply.github.com> Date: Wed, 15 Mar 2023 09:20:17 +0000 Subject: [PATCH 095/228] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adjustment to documentation, and families set declaration Co-authored-by: Félix David --- .../modules/kitsu/plugins/publish/integrate_kitsu_note.py | 4 ++-- website/docs/module_kitsu.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py index f394a3d04b..0bb2a63fea 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py @@ -77,11 +77,11 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): if self.set_status_note and allow_status_change: kitsu_status = gazu.task.get_task_status_by_short_name( self.note_status_shortname - families = set( + families = { instance.data.get("kitsu_task") for instance in context if instance.data.get("publish") - ) + } for instance in context: kitsu_task = instance.data.get("kitsu_task") diff --git a/website/docs/module_kitsu.md b/website/docs/module_kitsu.md index daa7102b79..9a85f83e3f 100644 --- a/website/docs/module_kitsu.md +++ b/website/docs/module_kitsu.md @@ -44,10 +44,10 @@ Task status can be automatically set during publish thanks to `Integrate Kitsu N `Admin -> Studio Settings -> Project Settings -> Kitsu -> Integrate Kitsu Note`. There are four settings available: -- `Set status on note` -> turns on and off this integrator. +- `Set status on note` -> Turns on and off this integrator. - `Note shortname` -> Which status shortname should be set automatically (Case sensitive). - `Status conditions` -> Conditions that need to be met for kitsu status to be changed. You can add as many conditions as you like. There are two fields to each conditions: `Condition` (Whether current status should be equal or not equal to the condition status) and `Short name` (Kitsu Shortname of the condition status). -- `Family requirements` -> With this option you can add requirements to which families must be pushed or not in order to have the task status set by this integrator. There are two fields for each requirements: `Condition` (Same as the above) and `Family` (name of the family concerned by this requirement). For instance, adding one item set to `Not equal` and `workfile`, would mean the task status would change if a subset from another family than workfile is published (workfile can still be included), but not if you only publish the workfile subset. +- `Family requirements` -> With this option you can add requirements to which families must be pushed or not in order to have the task status set by this integrator. There are two fields for each requirements: `Condition` (Same as the above) and `Family` (name of the family concerned by this requirement). For instance, adding one item set to `Not equal` and `workfile`, would mean the task status would change if a subset from another family than workfile is published (workfile can still be included), but not if you publish the workfile subset only. ![Integrate Kitsu Note project settings](assets/integrate_kitsu_note_settings.png) From 6433b1f270420f84a8742467dc34a51b71996e92 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 21 Mar 2023 16:52:13 +0100 Subject: [PATCH 096/228] Render only the local saver tools instead of all active savers in the comp --- .../plugins/publish/extract_render_local.py | 63 ++++++++++++++++--- 1 file changed, 56 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/extract_render_local.py b/openpype/hosts/fusion/plugins/publish/extract_render_local.py index 16a582032f..5a0140c525 100644 --- a/openpype/hosts/fusion/plugins/publish/extract_render_local.py +++ b/openpype/hosts/fusion/plugins/publish/extract_render_local.py @@ -1,7 +1,43 @@ +import logging +import contextlib import pyblish.api from openpype.hosts.fusion.api import comp_lock_and_undo_chunk +log = logging.getLogger(__name__) + + +@contextlib.contextmanager +def enabled_savers(comp, savers): + """Enable only the `savers` in Comp during the context. + + Any Saver tool in the passed composition that is not in the savers list + will be set to passthrough during the context. + + Args: + comp (object): Fusion composition object. + savers (list): List of Saver tool objects. + + """ + passthrough_key = "TOOLB_PassThrough" + original_states = {} + enabled_save_names = {saver.Name for saver in savers} + try: + all_savers = comp.GetToolList(False, "Saver").values() + for saver in all_savers: + original_state = saver.GetAttrs()[passthrough_key] + original_states[saver] = original_state + + # The passthrough state we want to set (passthrough != enabled) + state = saver.Name not in enabled_save_names + if state != original_state: + saver.SetAttrs({passthrough_key: state}) + yield + finally: + for saver, original_state in original_states.items(): + saver.SetAttrs({"TOOLB_PassThrough": original_state}) + + class FusionRenderLocal(pyblish.api.InstancePlugin): """Render the current Fusion composition locally.""" @@ -32,6 +68,16 @@ class FusionRenderLocal(pyblish.api.InstancePlugin): # 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__}" + + savers_to_render = [ + # Get the saver tool from the instance + instance[0] for instance in context if + # Only active instances + instance.data.get("publish", True) and + # Only render.local instances + "render.local" in instance.data["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 @@ -44,15 +90,18 @@ class FusionRenderLocal(pyblish.api.InstancePlugin): 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}") with comp_lock_and_undo_chunk(current_comp): - result = current_comp.Render( - { - "Start": frame_start, - "End": frame_end, - "Wait": True, - } - ) + with enabled_savers(current_comp, savers_to_render): + result = current_comp.Render( + { + "Start": frame_start, + "End": frame_end, + "Wait": True, + } + ) context.data[key] = bool(result) From 41d6269471f7f99c6323377fc6406ba1a0dfa59e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 21 Mar 2023 17:11:45 +0100 Subject: [PATCH 097/228] nuke: load nukenodes family timelessly --- .../hosts/nuke/plugins/load/load_backdrop.py | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/openpype/hosts/nuke/plugins/load/load_backdrop.py b/openpype/hosts/nuke/plugins/load/load_backdrop.py index f227aa161a..67c7877e60 100644 --- a/openpype/hosts/nuke/plugins/load/load_backdrop.py +++ b/openpype/hosts/nuke/plugins/load/load_backdrop.py @@ -54,22 +54,19 @@ class LoadBackdropNodes(load.LoaderPlugin): version = context['version'] version_data = version.get("data", {}) vname = version.get("name", None) - first = version_data.get("frameStart", None) - last = version_data.get("frameEnd", None) namespace = namespace or context['asset']['name'] colorspace = version_data.get("colorspace", None) object_name = "{}_{}".format(name, namespace) # prepare data for imprinting # add additional metadata from the version to imprint to Avalon knob - add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", - "source", "author", "fps"] + add_keys = ["source", "author", "fps"] - data_imprint = {"frameStart": first, - "frameEnd": last, - "version": vname, - "colorspaceInput": colorspace, - "objectName": object_name} + data_imprint = { + "version": vname, + "colorspaceInput": colorspace, + "objectName": object_name + } for k in add_keys: data_imprint.update({k: version_data[k]}) @@ -204,18 +201,13 @@ class LoadBackdropNodes(load.LoaderPlugin): name = container['name'] version_data = version_doc.get("data", {}) vname = version_doc.get("name", None) - first = version_data.get("frameStart", None) - last = version_data.get("frameEnd", None) namespace = container['namespace'] colorspace = version_data.get("colorspace", None) object_name = "{}_{}".format(name, namespace) - add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", - "source", "author", "fps"] + add_keys = ["source", "author", "fps"] data_imprint = {"representation": str(representation["_id"]), - "frameStart": first, - "frameEnd": last, "version": vname, "colorspaceInput": colorspace, "objectName": object_name} From 5ca5498b03bb1a88564b43e09fa4e660111614ec Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Tue, 21 Mar 2023 17:33:50 +0100 Subject: [PATCH 098/228] Changed family requirement check based on discussions --- .../plugins/publish/integrate_kitsu_note.py | 79 ++++++------------- 1 file changed, 23 insertions(+), 56 deletions(-) diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py index 0bb2a63fea..366e70934f 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py @@ -53,12 +53,8 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): continue kitsu_task = instance.data.get("kitsu_task") - if kitsu_task is None: + if not kitsu_task: continue - # Get comment text body - publish_comment = context.data.get("comment") - if not publish_comment: - self.log.info("Comment is not set.") # Get note status, by default uses the task status for the note # if it is not specified in the configuration @@ -74,57 +70,31 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): allow_status_change = False break - if self.set_status_note and allow_status_change: - kitsu_status = gazu.task.get_task_status_by_short_name( - self.note_status_shortname - families = { - instance.data.get("kitsu_task") - for instance in context - if instance.data.get("publish") - } + if allow_status_change: + # Get families + families = { + instance.data.get("kitsu_task") + for instance in context + if instance.data.get("publish") + } - for instance in context: - kitsu_task = instance.data.get("kitsu_task") - if not kitsu_task: - continue + # Check if any family requirement is met + for family_requirement in self.family_requirements: + condition = family_requirement["condition"] == "equal" - # Get note status, by default uses the task status for the note - # if it is not specified in the configuration - kitsu_task = context.data["kitsu_task"] - shortname = kitsu_task["task_status"]["short_name"].upper() - note_status = kitsu_task["task_status_id"] - if ( - self.set_status_note - and next( - ( - False - for status_except in self.status_exceptions - if shortname == status_except["short_name"].upper() - and status_except["condition"] == "equal" - or shortname != status_except["short_name"].upper() - and status_except["condition"] == "not_equal" - ), - True, - ) - and next( - ( - True - for family in families - if next( - ( - False - for family_req in self.family_requirements - if family_req["condition"] == "equal" - and family_req["family"].lower() != family - or family_req["condition"] == "not_equal" - and family_req["family"].lower() == family - ), - True, + for family in families: + match = ( + family_requirement["short_name"].lower() == family ) - ), - False, - ) - ): + if match and not condition or condition and not match: + allow_status_change = True + break + + if allow_status_change: + break + + # Set note status + if self.set_status_note and allow_status_change: kitsu_status = gazu.task.get_task_status_by_short_name( self.note_status_shortname ) @@ -153,9 +123,6 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): ) kitsu_comment = gazu.task.add_comment( kitsu_task, note_status, comment=publish_comment - instance.data["kitsu_task"], - note_status, - comment=publish_comment, ) instance.data["kitsu_comment"] = kitsu_comment From 109435fee281acd7be8bd2f2a67573e4221e8761 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Tue, 21 Mar 2023 17:48:01 +0100 Subject: [PATCH 099/228] Fixed kitsu project settings schema, updated documentation screenshot --- .../projects_schema/schema_project_kitsu.json | 1 - .../assets/integrate_kitsu_note_settings.png | Bin 44847 -> 46673 bytes 2 files changed, 1 deletion(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json b/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json index ba77f2b471..0a3a5e6e7b 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json @@ -127,7 +127,6 @@ "label": "Custom comment" } ] - } } ] } diff --git a/website/docs/assets/integrate_kitsu_note_settings.png b/website/docs/assets/integrate_kitsu_note_settings.png index fd055942d023e2ffc12fc508cd7079eb834045c3..6f76556afa54b4a1cd946ffb78d200ec3cd3e3b1 100644 GIT binary patch literal 46673 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%0D3V?)tF)Lx2^;H(ZQbbO&WO=KC?o7RUlAm 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
  • bzki{Py{Pds_+NLrzF>l&iL#!pA0lu=Pb|*x!3Ya10B1s6(fwHio`Y3yaj;;$ zSpO;pTMD}L!oUc5B$$9UZ>Xry9CqnoQ2#JDgmUBB-mIjn*kxAm2^h(8Qz)&95RJ%DcAH$#yOlU0il?itP#kFet zVX0*Pm=a~yqVi{q9+Fl9tPMKgOo85tq9;V{?&`dqQ`T_{?}s!LI$E-1{V_a z<;#vsPHSi9RNbM=V2O=UwH_n>B~g+y?dA_nO2E_?7kfx4cduBi`Qp(@c~BMEkV-F~ zfD&`QFqPA0o97ZHry$dsucNJo_g`)@$5PY9iUE&<7g|Sr#a712@4Ag-OuGxsMniHZ za4v83HW^&~xAgsKP5-;Tzc(qC^g2od zK}7GHodh!q(_Un%%2`c(4%-QUK|t6-GYB-hX>80?E-6{HzFg@zShZX1)@Cb8*j#A< zEYsoEXYzq1Z$JS#pE8v?TZ(O0VGX*OSy`A}-DqjCm)FfG>3-fe>b3es2@(G-N5Ad) z_BPVx*{xychLzNT3j^rj;2`{0^r~Mqs____v?Bt~)45@brV6etg}~3FPX?6OHh8Xy zNZe@{Yt8f1Z5_t7K{JJ+@qrYAGlgmK645k`RLJrziIew=bPCS1VtXxwN^+~qXe!SY z9H66D?s6$TRuNhlloQ(D>^CU&X3jfjxY{P#c}GtV14jlbqfM*J);pwq_kNCh%zeM? zw>9603)+@u30@Sg*L7Iu!|(@vIEuwIBx}R)`a-uYDo1nqoi%yELvc9G7Oh)N4sYyTSoVD7{H*p-T zcm*2c>PA~IoC12%sZjD4zRTru`gwm{kh10`m)m;+cu3HWnQzCu#%xhI9lNsnISh)> z;5DY@+duFKr(g`75W81oOUbU1`h=S4d|RD-BIsCh_2TB>@u||L`zh!zb*|&Odu6^W*5!Z zBAKf0Xl&d&yTv@z4M@bA(OTBhVG}P|Db6>Gi}6;o^wauIviqxtC#htc+F0{nx6foB z=IG^5ht=Yv0WEq-fLZ1V`)d_wO5C@bn#ZXlX~6M#jT-yj7b;j8|1>K2VR>2-ura!N zF%DiSP;;gS(yI@S5@&}en4@7q^716BtE-@N`gHpe7Z(b%ze<(|1?|9^h1bZ5HTLep zdZ*~2e(mH`7{FBuGb|tpU?~FC8`{bg zto1Fg2zD=LkH`0J+z|^Kd9cl=01o1m?Fr?N=C*O{^9^P+1=HKW;c;uaHb~z~4!9=@ zSS1Z8$Og-kR&0W^_Z-)Ji4uAs?nQl~8YBFv6@}c0nk@=Dd$44$276RwQDXRUcuAK4 zW+yRPOjqGVx-t^_R!Uen?qZOyEWx1sLjdHGo9E;pHpMr@{*_6{uLwz>hi&9 zxI}kbKmUL#T{(Jkl7+s=d8wkv&--X`kJGUO2g{UofO)?!#9vCA?UY$4c(0kMp z0}Ea`+z(i(CzZjE*Usiw;%1gBaqqISo@A;f7Ou&0bDG#eX*c1eR#d&(#^;Il+^Nxk;P=(h**;EW8$UFEG)$QBYdDJF|ZB2(LQ@z&l zvzTqIsOw$exrmz9zkt)U<#gm**Dkrb1y09??t=4*s{&qV6!*3?73Y&X}NFqIrsE^+$uhWrSF&( zanVkX(9SLVT8H#kslnCQGUHmL)JX{B-r4TW_NJIE9rzUs=m|qExgD0ot3EpLw&?9> z2;@8AP5wyK@{d%{#zH4zm>yUF*%X7Vl2LgdacjE-H zgGl-LXXPHuBp0&tx!l~>q?a8>o|>U-o~d}L2?%JzNgxou+_WZkoTufdS|<39OPpTo zo%h1o?|)pc7wRBwxVJR^EGSq~${<0C3<5E?h{k$73)el6k#TN3x?nx1BtN@GYY!v% zg?tPyOyKh%Lh%!)!MBjt6e{2wq|F*i>|k%s*#K@N1R{$6vlo6#kqxpiMtpfoNkcJT I!94Uo0HuA#d;kCd literal 44847 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 BT7Uom From 20219b465bc1c066bf3af5c53b0e2447450d1f5a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 22 Mar 2023 19:57:45 +0800 Subject: [PATCH 100/228] resolution setting updated --- openpype/hosts/max/api/lib.py | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 6a7f63ca75..b6e3958797 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -6,12 +6,10 @@ from pymxs import runtime as rt from typing import Union import contextlib -from openpype.client import ( - get_project +from openpype.pipeline.context_tools import ( + get_current_project_asset, + get_current_project ) -from openpype.pipeline import legacy_io - -from openpype.pipeline.context_tools import get_current_project_asset JSON_PREFIX = "JSON::" @@ -164,7 +162,7 @@ def get_multipass_setting(project_setting=None): ["multipass"]) -def set_scene_resolution(width, height): +def set_scene_resolution(width: int, height: int): """Set the render resolution Args: @@ -188,19 +186,15 @@ def reset_scene_resolution(): Returns: None """ - project_name = legacy_io.active_project() - project_doc = get_project(project_name) - project_data = project_doc["data"] - asset_data = get_current_project_asset()["data"] - + project_resolution_data = get_current_project(fields=["data.resolutionWidth", + "data.resolutionHeight"])["data"] + asset_resolution_data = get_current_project_asset(fields=["data.resolutionWidth", + "data.resolutionHeight"])["data"] # Set project resolution - width_key = "resolutionWidth" - height_key = "resolutionHeight" - proj_width_key = project_data.get(width_key, 1920) - proj_height_key = project_data.get(height_key, 1080) - - width = asset_data.get(width_key, proj_width_key) - height = asset_data.get(height_key, proj_height_key) + project_width = int(project_resolution_data.get("resolutionWidth", 1920)) + project_height = int(project_resolution_data.get("resolutionHeight", 1080)) + width = int(asset_resolution_data.get("resolutionWidth", project_width)) + height = int(asset_resolution_data.get("resolutionHeight", project_height)) set_scene_resolution(width, height) From 0bce4762f12e10e3097c5acdafe746a3b1cc71cd Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 22 Mar 2023 20:00:23 +0800 Subject: [PATCH 101/228] hound fix --- openpype/hosts/max/api/lib.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index b6e3958797..577dc6e99d 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -186,10 +186,11 @@ def reset_scene_resolution(): Returns: None """ - project_resolution_data = get_current_project(fields=["data.resolutionWidth", - "data.resolutionHeight"])["data"] - asset_resolution_data = get_current_project_asset(fields=["data.resolutionWidth", - "data.resolutionHeight"])["data"] + data = ["data.resolutionWidth","data.resolutionHeight"] + project_resolution = get_current_project(fields=data)["data"] + project_resolution_data = project_resolution["data"] + asset_resolution = get_current_project_asset(fields=data)["data"] + asset_resolution_data = asset_resolution["data"] # Set project resolution project_width = int(project_resolution_data.get("resolutionWidth", 1920)) project_height = int(project_resolution_data.get("resolutionHeight", 1080)) From e70be13d9650c6c28239cb1578c06751c5e670c5 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 22 Mar 2023 20:00:59 +0800 Subject: [PATCH 102/228] hound fix --- openpype/hosts/max/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 577dc6e99d..ac1d158792 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -186,7 +186,7 @@ def reset_scene_resolution(): Returns: None """ - data = ["data.resolutionWidth","data.resolutionHeight"] + data = ["data.resolutionWidth", "data.resolutionHeight"] project_resolution = get_current_project(fields=data)["data"] project_resolution_data = project_resolution["data"] asset_resolution = get_current_project_asset(fields=data)["data"] From e9eb09e100d990e5a6d8648ff32df0d3e7ec4969 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 22 Mar 2023 12:58:33 +0000 Subject: [PATCH 103/228] Initial working version --- .../maya/plugins/publish/collect_review.py | 23 +++-- openpype/lib/execute.py | 7 ++ openpype/plugins/publish/extract_burnin.py | 4 + openpype/scripts/otio_burnin.py | 87 +++++++++++++++++-- 4 files changed, 109 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_review.py b/openpype/hosts/maya/plugins/publish/collect_review.py index 80fd5b0dbd..3d3ee09eed 100644 --- a/openpype/hosts/maya/plugins/publish/collect_review.py +++ b/openpype/hosts/maya/plugins/publish/collect_review.py @@ -146,11 +146,24 @@ class CollectReview(pyblish.api.InstancePlugin): ) # Collect focal length. - data = { - "cameraFocalLength": cmds.getAttr(camera + ".focalLength") - } + #Refactor to lib or use available lib method. + plug = "{0}.focalLength".format(camera) + focal_length = None + if not cmds.listConnections(plug, destination=False, source=True): + # Static. + focal_length = cmds.getAttr(plug) + else: + # Dynamic. + start = instance.data["frameStart"] + end = instance.data["frameEnd"] + 1 + focal_length = [ + cmds.getAttr(plug, time=t) for t in range(int(start), int(end)) + ] + + key = "focalLength" + instance.data[key] = focal_length try: - instance.data["customData"].update(data) + instance.data["burninDataMembers"].append(key) except KeyError: - instance.data["customData"] = data + instance.data["burninDataMembers"] = [key] diff --git a/openpype/lib/execute.py b/openpype/lib/execute.py index 759a4db0cb..5ba62f177f 100644 --- a/openpype/lib/execute.py +++ b/openpype/lib/execute.py @@ -8,6 +8,8 @@ import tempfile from .log import Logger from .vendor_bin_utils import find_executable +from .openpype_version import is_running_from_build + # MSDN process creation flag (Windows only) CREATE_NO_WINDOW = 0x08000000 @@ -196,6 +198,11 @@ def run_openpype_process(*args, **kwargs): # Skip envs that can affect OpenPype process # - fill more if you find more env = clean_envs_for_openpype_process(os.environ) + + # Add OpenPype version if we are running from build. + if not is_running_from_build(): + env.pop("OPENPYPE_VERSION", None) + return run_subprocess(args, env=env, **kwargs) diff --git a/openpype/plugins/publish/extract_burnin.py b/openpype/plugins/publish/extract_burnin.py index f113e61bb0..de876c8486 100644 --- a/openpype/plugins/publish/extract_burnin.py +++ b/openpype/plugins/publish/extract_burnin.py @@ -254,6 +254,10 @@ class ExtractBurnin(publish.Extractor): # Add context data burnin_data. burnin_data["custom"] = custom_data + # Add data members. + for key in instance.data.get("burninDataMembers", []): + burnin_data[key] = instance.data[key] + # Add source camera name to burnin data camera_name = repre.get("camera_name") if camera_name: diff --git a/openpype/scripts/otio_burnin.py b/openpype/scripts/otio_burnin.py index cb4646c099..bb7a208103 100644 --- a/openpype/scripts/otio_burnin.py +++ b/openpype/scripts/otio_burnin.py @@ -4,8 +4,10 @@ import re import subprocess import platform import json -import opentimelineio_contrib.adapters.ffmpeg_burnins as ffmpeg_burnins +import tempfile +from string import Formatter +import opentimelineio_contrib.adapters.ffmpeg_burnins as ffmpeg_burnins from openpype.lib import ( get_ffmpeg_tool_path, get_ffmpeg_codec_args, @@ -23,7 +25,7 @@ FFMPEG = ( ).format(ffmpeg_path) DRAWTEXT = ( - "drawtext=fontfile='%(font)s':text=\\'%(text)s\\':" + "drawtext@'%(label)s'=fontfile='%(font)s':text=\\'%(text)s\\':" "x=%(x)s:y=%(y)s:fontcolor=%(color)s@%(opacity).1f:fontsize=%(size)d" ) TIMECODE = ( @@ -39,6 +41,34 @@ TIMECODE_KEY = "{timecode}" SOURCE_TIMECODE_KEY = "{source_timecode}" +def convert_list_to_cmd(list_to_convert, fps, label=""): + path = None + #need to clean up temp file when done + with tempfile.NamedTemporaryFile(mode="w", delete=False) as f: + for i, value in enumerate(list_to_convert): + seconds = i / fps + + # Escape special character + value = str(value).replace(":", "\\:") + + filter = "drawtext" + if label: + filter += "@" + label + + line = ( + "{start} {filter} reinit text='{value}';" + "\n".format(start=seconds, filter=filter, value=value) + ) + + f.write(line) + f.flush() + path = f.name + path = path.replace("\\", "/") + path = path.replace(":", "\\:") + + return "sendcmd=f='{}'".format(path) + + def _get_ffprobe_data(source): """Reimplemented from otio burnins to be able use full path to ffprobe :param str source: source media file @@ -144,7 +174,7 @@ class ModifiedBurnins(ffmpeg_burnins.Burnins): self.options_init.update(options_init) def add_text( - self, text, align, frame_start=None, frame_end=None, options=None + self, text, align, frame_start=None, frame_end=None, options=None, cmd="" ): """ Adding static text to a filter. @@ -165,7 +195,13 @@ class ModifiedBurnins(ffmpeg_burnins.Burnins): if frame_end is not None: options["frame_end"] = frame_end - self._add_burnin(text, align, options, DRAWTEXT) + draw_text = DRAWTEXT + if cmd: + draw_text = "{}, {}".format(cmd, DRAWTEXT) + + options["label"] = align + + self._add_burnin(text, align, options, draw_text) def add_timecode( self, align, frame_start=None, frame_end=None, frame_start_tc=None, @@ -501,7 +537,7 @@ def burnins_from_data( if not value: continue - if isinstance(value, (dict, list, tuple)): + if isinstance(value, (dict, tuple)): raise TypeError(( "Expected string or number type." " Got: {} - \"{}\"" @@ -573,8 +609,43 @@ def burnins_from_data( burnin.add_timecode(*args) continue - text = value.format(**data) - burnin.add_text(text, align, frame_start, frame_end) + cmd = "" + text = None + keys = [i[1] for i in Formatter().parse(value) if i[1] is not None] + list_to_convert = [] + + # Warn about nested dictionary support for lists. Ei. we dont support + # it. + if "[" in "".join(keys): + print( + "We dont support converting nested dictionaries to lists," + " so skipping {}".format(value) + ) + else: + for key in keys: + data_value = data[key] + + # Multiple lists are not supported. + if isinstance(data_value, list) and list_to_convert: + raise ValueError( + "Found multiple lists to convert, which is not " + "supported: {}".format(value) + ) + + if isinstance(data_value, list): + print("Found list to convert: {}".format(data_value)) + for v in data_value: + data[key] = v + list_to_convert.append(value.format(**data)) + + if list_to_convert: + text = list_to_convert[0] + cmd = convert_list_to_cmd(list_to_convert, 25.0, label=align)# need to fetch fps properly + print("cmd: " + cmd) + else: + text = value.format(**data) + print(text) + burnin.add_text(text, align, frame_start, frame_end, cmd=cmd) ffmpeg_args = [] if codec_data: @@ -613,6 +684,8 @@ if __name__ == "__main__": with open(in_data_json_path, "r") as file_stream: in_data = json.load(file_stream) + print(json.dumps(in_data, indent=4, sort_keys=True)) + burnins_from_data( in_data["input"], in_data["output"], From 583fecd4009003681fb6cc535aecdbe641a66022 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Wed, 22 Mar 2023 14:37:59 +0100 Subject: [PATCH 104/228] Fixes and adjustments based on reviews --- .../plugins/publish/integrate_kitsu_note.py | 18 ++-- .../defaults/project_settings/kitsu.json | 6 +- .../projects_schema/schema_project_kitsu.json | 97 ++++++++++-------- .../assets/integrate_kitsu_note_settings.png | Bin 46673 -> 48874 bytes website/docs/module_kitsu.md | 4 +- 5 files changed, 69 insertions(+), 56 deletions(-) diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py index 366e70934f..a5253d7878 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py @@ -14,8 +14,10 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): # status settings set_status_note = False note_status_shortname = "wfa" - status_conditions = list() - family_requirements = list() + status_change_conditions = { + "status_conditions": [], + "family_requirements": [], + } # comment settings custom_comment_template = { @@ -63,7 +65,7 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): # Check if any status condition is not met allow_status_change = True - for status_cond in self.status_conditions: + for status_cond in self.status_change_conditions["status_conditions"]: condition = status_cond["condition"] == "equal" match = status_cond["short_name"].upper() == shortname if match and not condition or condition and not match: @@ -73,20 +75,22 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): if allow_status_change: # Get families families = { - instance.data.get("kitsu_task") + instance.data.get("family") for instance in context if instance.data.get("publish") } + allow_status_change = False + # Check if any family requirement is met - for family_requirement in self.family_requirements: + for family_requirement in self.status_change_conditions["family_requirements"]: condition = family_requirement["condition"] == "equal" for family in families: match = ( - family_requirement["short_name"].lower() == family + family_requirement["family"].lower() == family ) - if match and not condition or condition and not match: + if match and condition or not condition and not match: allow_status_change = True break diff --git a/openpype/settings/defaults/project_settings/kitsu.json b/openpype/settings/defaults/project_settings/kitsu.json index 32c6c253c7..59a36d8b97 100644 --- a/openpype/settings/defaults/project_settings/kitsu.json +++ b/openpype/settings/defaults/project_settings/kitsu.json @@ -8,8 +8,10 @@ "IntegrateKitsuNote": { "set_status_note": false, "note_status_shortname": "wfa", - "status_conditions": [], - "family_requirements": [], + "status_change_conditions": { + "status_conditions": [], + "family_requirements": [] + }, "custom_comment_template": { "enabled": false, "comment_template": "{comment}\n\n| | |\n|--|--|\n| version| `{version}` |\n| family | `{family}` |\n| name | `{name}` |" diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json b/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json index 0a3a5e6e7b..8aeed00542 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json @@ -54,55 +54,62 @@ "label": "Note shortname" }, { - "type": "list", - "key": "status_conditions", - "label": "Status conditions", - "object_type": { - "type": "dict", - "key": "conditions_dict", - "children": [ - { - "type": "enum", - "key": "condition", - "label": "Condition", - "enum_items": [ - {"equal": "Equal"}, - {"not_equal": "Not equal"} + "type": "dict", + "collapsible": true, + "key": "status_change_conditions", + "label": "Status change conditions", + "children": [ + { + "type": "list", + "key": "status_conditions", + "label": "Status conditions", + "object_type": { + "type": "dict", + "key": "condition_dict", + "children": [ + { + "type": "enum", + "key": "condition", + "label": "Condition", + "enum_items": [ + {"equal": "Equal"}, + {"not_equal": "Not equal"} + ] + }, + { + "type": "text", + "key": "short_name", + "label": "Short name" + } ] - }, - { - "type": "text", - "key": "short_name", - "label": "Short name" } - ] - }, - "label": "Status shortname" - }, - { - "type": "list", - "key": "family_requirements", - "label": "Family requirements", - "object_type": { - "type": "dict", - "key": "requirement_dict", - "children": [ - { - "type": "enum", - "key": "condition", - "label": "Condition", - "enum_items": [ - {"equal": "Equal"}, - {"not_equal": "Not equal"} + }, + { + "type": "list", + "key": "family_requirements", + "label": "Family requirements", + "object_type": { + "type": "dict", + "key": "requirement_dict", + "children": [ + { + "type": "enum", + "key": "condition", + "label": "Condition", + "enum_items": [ + {"equal": "Equal"}, + {"not_equal": "Not equal"} + ] + }, + { + "type": "text", + "key": "family", + "label": "Family" + } ] - }, - { - "type": "text", - "key": "family", - "label": "Family" } - ] - } + } + ] }, { "type": "dict", diff --git a/website/docs/assets/integrate_kitsu_note_settings.png b/website/docs/assets/integrate_kitsu_note_settings.png index 6f76556afa54b4a1cd946ffb78d200ec3cd3e3b1..9c59f0bf5cc9a748de1a8b66b248f539ee8014b5 100644 GIT binary patch literal 48874 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%0D3V?)tF)Lx2^;H(ZQbbO&WO=KC?o7RUlAm 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

  • bzki{Py{Pds_+NLrzF>l&iL#!pA0lu=Pb|*x!3Ya10B1s6(fwHio`Y3yaj;;$ zSpO;pTMD}L!oUc5B$$9UZ>Xry9CqnoQ2#JDgmUBB-mIjn*kxAm2^h(8Qz)&95RJ%DcAH$#yOlU0il?itP#kFet zVX0*Pm=a~yqVi{q9+Fl9tPMKgOo85tq9;V{?&`dqQ`T_{?}s!LI$E-1{V_a z<;#vsPHSi9RNbM=V2O=UwH_n>B~g+y?dA_nO2E_?7kfx4cduBi`Qp(@c~BMEkV-F~ zfD&`QFqPA0o97ZHry$dsucNJo_g`)@$5PY9iUE&<7g|Sr#a712@4Ag-OuGxsMniHZ za4v83HW^&~xAgsKP5-;Tzc(qC^g2od zK}7GHodh!q(_Un%%2`c(4%-QUK|t6-GYB-hX>80?E-6{HzFg@zShZX1)@Cb8*j#A< zEYsoEXYzq1Z$JS#pE8v?TZ(O0VGX*OSy`A}-DqjCm)FfG>3-fe>b3es2@(G-N5Ad) z_BPVx*{xychLzNT3j^rj;2`{0^r~Mqs____v?Bt~)45@brV6etg}~3FPX?6OHh8Xy zNZe@{Yt8f1Z5_t7K{JJ+@qrYAGlgmK645k`RLJrziIew=bPCS1VtXxwN^+~qXe!SY z9H66D?s6$TRuNhlloQ(D>^CU&X3jfjxY{P#c}GtV14jlbqfM*J);pwq_kNCh%zeM? zw>9603)+@u30@Sg*L7Iu!|(@vIEuwIBx}R)`a-uYDo1nqoi%yELvc9G7Oh)N4sYyTSoVD7{H*p-T zcm*2c>PA~IoC12%sZjD4zRTru`gwm{kh10`m)m;+cu3HWnQzCu#%xhI9lNsnISh)> z;5DY@+duFKr(g`75W81oOUbU1`h=S4d|RD-BIsCh_2TB>@u||L`zh!zb*|&Odu6^W*5!Z zBAKf0Xl&d&yTv@z4M@bA(OTBhVG}P|Db6>Gi}6;o^wauIviqxtC#htc+F0{nx6foB z=IG^5ht=Yv0WEq-fLZ1V`)d_wO5C@bn#ZXlX~6M#jT-yj7b;j8|1>K2VR>2-ura!N zF%DiSP;;gS(yI@S5@&}en4@7q^716BtE-@N`gHpe7Z(b%ze<(|1?|9^h1bZ5HTLep zdZ*~2e(mH`7{FBuGb|tpU?~FC8`{bg zto1Fg2zD=LkH`0J+z|^Kd9cl=01o1m?Fr?N=C*O{^9^P+1=HKW;c;uaHb~z~4!9=@ zSS1Z8$Og-kR&0W^_Z-)Ji4uAs?nQl~8YBFv6@}c0nk@=Dd$44$276RwQDXRUcuAK4 zW+yRPOjqGVx-t^_R!Uen?qZOyEWx1sLjdHGo9E;pHpMr@{*_6{uLwz>hi&9 zxI}kbKmUL#T{(Jkl7+s=d8wkv&--X`kJGUO2g{UofO)?!#9vCA?UY$4c(0kMp z0}Ea`+z(i(CzZjE*Usiw;%1gBaqqISo@A;f7Ou&0bDG#eX*c1eR#d&(#^;Il+^Nxk;P=(h**;EW8$UFEG)$QBYdDJF|ZB2(LQ@z&l zvzTqIsOw$exrmz9zkt)U<#gm**Dkrb1y09??t=4*s{&qV6!*3?73Y&X}NFqIrsE^+$uhWrSF&( zanVkX(9SLVT8H#kslnCQGUHmL)JX{B-r4TW_NJIE9rzUs=m|qExgD0ot3EpLw&?9> z2;@8AP5wyK@{d%{#zH4zm>yUF*%X7Vl2LgdacjE-H zgGl-LXXPHuBp0&tx!l~>q?a8>o|>U-o~d}L2?%JzNgxou+_WZkoTufdS|<39OPpTo zo%h1o?|)pc7wRBwxVJR^EGSq~${<0C3<5E?h{k$73)el6k#TN3x?nx1BtN@GYY!v% zg?tPyOyKh%Lh%!)!MBjt6e{2wq|F*i>|k%s*#K@N1R{$6vlo6#kqxpiMtpfoNkcJT I!94Uo0HuA#d;kCd diff --git a/website/docs/module_kitsu.md b/website/docs/module_kitsu.md index 9a85f83e3f..05cff87fcc 100644 --- a/website/docs/module_kitsu.md +++ b/website/docs/module_kitsu.md @@ -46,8 +46,8 @@ Task status can be automatically set during publish thanks to `Integrate Kitsu N There are four settings available: - `Set status on note` -> Turns on and off this integrator. - `Note shortname` -> Which status shortname should be set automatically (Case sensitive). -- `Status conditions` -> Conditions that need to be met for kitsu status to be changed. You can add as many conditions as you like. There are two fields to each conditions: `Condition` (Whether current status should be equal or not equal to the condition status) and `Short name` (Kitsu Shortname of the condition status). -- `Family requirements` -> With this option you can add requirements to which families must be pushed or not in order to have the task status set by this integrator. There are two fields for each requirements: `Condition` (Same as the above) and `Family` (name of the family concerned by this requirement). For instance, adding one item set to `Not equal` and `workfile`, would mean the task status would change if a subset from another family than workfile is published (workfile can still be included), but not if you publish the workfile subset only. +- `Status change conditions - Status conditions` -> Conditions that need to be met for kitsu status to be changed. You can add as many conditions as you like. There are two fields to each conditions: `Condition` (Whether current status should be equal or not equal to the condition status) and `Short name` (Kitsu Shortname of the condition status). +- `Status change conditions - Family requirements` -> With this option you can add requirements to which families must be pushed or not in order to have the task status set by this integrator. There are two fields for each requirements: `Condition` (Same as the above) and `Family` (name of the family concerned by this requirement). For instance, adding one item set to `Not equal` and `workfile`, would mean the task status would change if a subset from another family than workfile is published (workfile can still be included), but not if you publish the workfile subset only. ![Integrate Kitsu Note project settings](assets/integrate_kitsu_note_settings.png) From 3af273679011d230d48aaa97a8d8468e69944c70 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Wed, 22 Mar 2023 14:38:42 +0100 Subject: [PATCH 105/228] linted --- .../kitsu/plugins/publish/integrate_kitsu_note.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py index a5253d7878..6a1b09dde1 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py @@ -65,7 +65,9 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): # Check if any status condition is not met allow_status_change = True - for status_cond in self.status_change_conditions["status_conditions"]: + for status_cond in self.status_change_conditions[ + "status_conditions" + ]: condition = status_cond["condition"] == "equal" match = status_cond["short_name"].upper() == shortname if match and not condition or condition and not match: @@ -83,13 +85,13 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): allow_status_change = False # Check if any family requirement is met - for family_requirement in self.status_change_conditions["family_requirements"]: + for family_requirement in self.status_change_conditions[ + "family_requirements" + ]: condition = family_requirement["condition"] == "equal" for family in families: - match = ( - family_requirement["family"].lower() == family - ) + match = family_requirement["family"].lower() == family if match and condition or not condition and not match: allow_status_change = True break From d861e556319bcc4f34fba481d6ec1e0aef9fbc84 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 22 Mar 2023 21:49:00 +0800 Subject: [PATCH 106/228] update the frame range settings --- openpype/hosts/max/api/lib.py | 72 ++++++++++++++--------------------- 1 file changed, 28 insertions(+), 44 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index f1d1f91dd1..ac8f6aedfe 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -6,13 +6,10 @@ from pymxs import runtime as rt from typing import Union import contextlib -from openpype.client import ( - get_project, - get_asset_by_name +from openpype.pipeline.context_tools import ( + get_current_project_asset, + get_current_project ) -from openpype.pipeline import legacy_io - -from openpype.pipeline.context_tools import get_current_project_asset JSON_PREFIX = "JSON::" @@ -165,7 +162,7 @@ def get_multipass_setting(project_setting=None): ["multipass"]) -def set_scene_resolution(width, height): +def set_scene_resolution(width: int, height: int): """Set the render resolution Args: @@ -189,49 +186,43 @@ def reset_scene_resolution(): Returns: None """ - project_name = legacy_io.active_project() - project_doc = get_project(project_name) - project_data = project_doc["data"] - asset_data = get_current_project_asset()["data"] - + data = ["data.resolutionWidth", "data.resolutionHeight"] + project_resolution = get_current_project(fields=data)["data"] + project_resolution_data = project_resolution["data"] + asset_resolution = get_current_project_asset(fields=data)["data"] + asset_resolution_data = asset_resolution["data"] # Set project resolution - width_key = "resolutionWidth" - height_key = "resolutionHeight" - proj_width_key = project_data.get(width_key, 1920) - proj_height_key = project_data.get(height_key, 1080) - - width = asset_data.get(width_key, proj_width_key) - height = asset_data.get(height_key, proj_height_key) + project_width = int(project_resolution_data.get("resolutionWidth", 1920)) + project_height = int(project_resolution_data.get("resolutionHeight", 1080)) + width = int(asset_resolution_data.get("resolutionWidth", project_width)) + height = int(asset_resolution_data.get("resolutionHeight", project_height)) set_scene_resolution(width, height) -def get_frame_range(): - """Get the current assets frame range and handles.""" - # Set frame start/end - project_name = legacy_io.active_project() - asset_name = legacy_io.Session["AVALON_ASSET"] - asset = get_asset_by_name(project_name, asset_name) +def get_frame_range() -> dict: + """Get the current assets frame range and handles. + Returns: + dict: with frame start, frame end, handle start, handle end. + """ + # Set frame start/end + asset = get_current_project_asset() frame_start = asset["data"].get("frameStart") frame_end = asset["data"].get("frameEnd") # Backwards compatibility if frame_start is None or frame_end is None: frame_start = asset["data"].get("edit_in") frame_end = asset["data"].get("edit_out") - if frame_start is None or frame_end is None: return - handles = asset["data"].get("handles") or 0 handle_start = asset["data"].get("handleStart") if handle_start is None: handle_start = handles - handle_end = asset["data"].get("handleEnd") if handle_end is None: handle_end = handles - return { "frameStart": frame_start, "frameEnd": frame_end, @@ -240,29 +231,24 @@ def get_frame_range(): } -def reset_frame_range(fps=True): - """Set frame range to current asset +def reset_frame_range(fps: bool=True): + """Set frame range to current asset. - Args: + This is part of 3dsmax documentation: animationRange: A System Global variable which lets you get and - set an Interval value that defines the start and end frames - of the Active Time Segment. + set an Interval value that defines the start and end frames + of the Active Time Segment. frameRate: A System Global variable which lets you get - and set an Integer value that defines the current - scene frame rate in frames-per-second. + and set an Integer value that defines the current + scene frame rate in frames-per-second. """ if fps: - fps_number = float(legacy_io.Session.get("AVALON_FPS", - 25)) + fps_number = float(get_current_project(fields=["data.fps"])["data"]["fps"]) rt.frameRate = fps_number - frame_range = get_frame_range() - frame_start = frame_range["frameStart"] - int(frame_range["handleStart"]) frame_end = frame_range["frameEnd"] + int(frame_range["handleEnd"]) - frange_cmd = f"animationRange = interval {frame_start} {frame_end}" - rt.execute(frange_cmd) @@ -281,8 +267,6 @@ def set_context_setting(): """ reset_scene_resolution() - reset_frame_range() - def get_max_version(): """ From 4e8354eec810023d83a23616c1af0d76035932a6 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 22 Mar 2023 21:54:43 +0800 Subject: [PATCH 107/228] hound fix --- openpype/hosts/max/api/lib.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index ac8f6aedfe..09edaaa2ef 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -182,7 +182,6 @@ def reset_scene_resolution(): scene resolution can be overwritten by an asset if the asset.data contains any information regarding scene resolution . - Returns: None """ @@ -204,7 +203,7 @@ def get_frame_range() -> dict: """Get the current assets frame range and handles. Returns: - dict: with frame start, frame end, handle start, handle end. + dict: with frame start, frame end, handle start, handle end. """ # Set frame start/end asset = get_current_project_asset() @@ -233,17 +232,18 @@ def get_frame_range() -> dict: def reset_frame_range(fps: bool=True): """Set frame range to current asset. + This is part of 3dsmax documentation: - This is part of 3dsmax documentation: - animationRange: A System Global variable which lets you get and - set an Interval value that defines the start and end frames - of the Active Time Segment. - frameRate: A System Global variable which lets you get - and set an Integer value that defines the current - scene frame rate in frames-per-second. + animationRange: A System Global variable which lets you get and + set an Interval value that defines the start and end frames + of the Active Time Segment. + frameRate: A System Global variable which lets you get + and set an Integer value that defines the current + scene frame rate in frames-per-second. """ if fps: - fps_number = float(get_current_project(fields=["data.fps"])["data"]["fps"]) + data_fps = get_current_project(fields=["data.fps"]) + fps_number = float(data_fps["data"]["fps"]) rt.frameRate = fps_number frame_range = get_frame_range() frame_start = frame_range["frameStart"] - int(frame_range["handleStart"]) From e373ccf4e3a747d3db3266f56478cdacfeb0aa3c Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 22 Mar 2023 21:57:19 +0800 Subject: [PATCH 108/228] hound fix --- openpype/hosts/max/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 09edaaa2ef..c398d7cf48 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -230,7 +230,7 @@ def get_frame_range() -> dict: } -def reset_frame_range(fps: bool=True): +def reset_frame_range(fps:bool=True): """Set frame range to current asset. This is part of 3dsmax documentation: From 57308ff488e0a2f931231a455cad13ca23eaaa7e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 22 Mar 2023 21:58:29 +0800 Subject: [PATCH 109/228] hound fix --- openpype/hosts/max/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index c398d7cf48..aa7f72b26c 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -230,7 +230,7 @@ def get_frame_range() -> dict: } -def reset_frame_range(fps:bool=True): +def reset_frame_range(fps: bool = True): """Set frame range to current asset. This is part of 3dsmax documentation: From 9eadc11941f4cffc6b1061f56f9751efd848c7de Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Wed, 22 Mar 2023 14:58:14 +0000 Subject: [PATCH 110/228] Update openpype/scripts/otio_burnin.py Co-authored-by: Roy Nieterau --- openpype/scripts/otio_burnin.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/scripts/otio_burnin.py b/openpype/scripts/otio_burnin.py index bb7a208103..e7c165ba0e 100644 --- a/openpype/scripts/otio_burnin.py +++ b/openpype/scripts/otio_burnin.py @@ -684,8 +684,6 @@ if __name__ == "__main__": with open(in_data_json_path, "r") as file_stream: in_data = json.load(file_stream) - print(json.dumps(in_data, indent=4, sort_keys=True)) - burnins_from_data( in_data["input"], in_data["output"], From 22dac59ccf3d6269ec582c15de387fb23bc0e8e9 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 22 Mar 2023 14:58:46 +0000 Subject: [PATCH 111/228] Store burnin data as dictionary directly. --- openpype/hosts/maya/plugins/publish/collect_review.py | 6 ++---- openpype/plugins/publish/extract_burnin.py | 3 +-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_review.py b/openpype/hosts/maya/plugins/publish/collect_review.py index 3d3ee09eed..0857e8224d 100644 --- a/openpype/hosts/maya/plugins/publish/collect_review.py +++ b/openpype/hosts/maya/plugins/publish/collect_review.py @@ -161,9 +161,7 @@ class CollectReview(pyblish.api.InstancePlugin): ] key = "focalLength" - instance.data[key] = focal_length - try: - instance.data["burninDataMembers"].append(key) + instance.data["burninDataMembers"][key] = focal_length except KeyError: - instance.data["burninDataMembers"] = [key] + instance.data["burninDataMembers"] = {key: focal_length} diff --git a/openpype/plugins/publish/extract_burnin.py b/openpype/plugins/publish/extract_burnin.py index de876c8486..8d29f814b5 100644 --- a/openpype/plugins/publish/extract_burnin.py +++ b/openpype/plugins/publish/extract_burnin.py @@ -255,8 +255,7 @@ class ExtractBurnin(publish.Extractor): burnin_data["custom"] = custom_data # Add data members. - for key in instance.data.get("burninDataMembers", []): - burnin_data[key] = instance.data[key] + burnin_data.update(instance.data.get("burninDataMembers", {})) # Add source camera name to burnin data camera_name = repre.get("camera_name") From f0bdc76183098782b1b42b37db8bb9386f3f864a Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 22 Mar 2023 14:59:40 +0000 Subject: [PATCH 112/228] Clean up prints --- openpype/scripts/otio_burnin.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/scripts/otio_burnin.py b/openpype/scripts/otio_burnin.py index e7c165ba0e..fa3f1e3389 100644 --- a/openpype/scripts/otio_burnin.py +++ b/openpype/scripts/otio_burnin.py @@ -641,10 +641,9 @@ def burnins_from_data( if list_to_convert: text = list_to_convert[0] cmd = convert_list_to_cmd(list_to_convert, 25.0, label=align)# need to fetch fps properly - print("cmd: " + cmd) else: text = value.format(**data) - print(text) + burnin.add_text(text, align, frame_start, frame_end, cmd=cmd) ffmpeg_args = [] From 266992f93ba45706e126358dfc47d9a18aaf015f Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Wed, 22 Mar 2023 15:00:05 +0000 Subject: [PATCH 113/228] Update openpype/scripts/otio_burnin.py Co-authored-by: Roy Nieterau --- openpype/scripts/otio_burnin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/scripts/otio_burnin.py b/openpype/scripts/otio_burnin.py index e7c165ba0e..5e32e8d267 100644 --- a/openpype/scripts/otio_burnin.py +++ b/openpype/scripts/otio_burnin.py @@ -537,7 +537,7 @@ def burnins_from_data( if not value: continue - if isinstance(value, (dict, tuple)): + if isinstance(value, dict): raise TypeError(( "Expected string or number type." " Got: {} - \"{}\"" From 4f334a1f8061dfb8d38b176f2b8468e33df26c23 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 22 Mar 2023 15:02:31 +0000 Subject: [PATCH 114/228] Update type error message --- openpype/scripts/otio_burnin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/scripts/otio_burnin.py b/openpype/scripts/otio_burnin.py index 58727bc6b8..142ab748af 100644 --- a/openpype/scripts/otio_burnin.py +++ b/openpype/scripts/otio_burnin.py @@ -539,7 +539,7 @@ def burnins_from_data( if isinstance(value, dict): raise TypeError(( - "Expected string or number type." + "Expected string, number or list type." " Got: {} - \"{}\"" " (Make sure you have new burnin presets)." ).format(str(type(value)), str(value))) From 4f0aafe560be44bd42baff8946ff5d336bf31714 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 22 Mar 2023 15:08:32 +0000 Subject: [PATCH 115/228] Get FPS properly --- openpype/scripts/otio_burnin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/scripts/otio_burnin.py b/openpype/scripts/otio_burnin.py index 142ab748af..ee29d240b2 100644 --- a/openpype/scripts/otio_burnin.py +++ b/openpype/scripts/otio_burnin.py @@ -640,7 +640,9 @@ def burnins_from_data( if list_to_convert: text = list_to_convert[0] - cmd = convert_list_to_cmd(list_to_convert, 25.0, label=align)# need to fetch fps properly + cmd = convert_list_to_cmd( + list_to_convert, data["fps"], label=align + ) else: text = value.format(**data) From a364a82b7dddf64bb8178c3e6b93ccc0565328d6 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 22 Mar 2023 15:10:02 +0000 Subject: [PATCH 116/228] Hound --- openpype/scripts/otio_burnin.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/openpype/scripts/otio_burnin.py b/openpype/scripts/otio_burnin.py index ee29d240b2..4bbb684deb 100644 --- a/openpype/scripts/otio_burnin.py +++ b/openpype/scripts/otio_burnin.py @@ -174,7 +174,13 @@ class ModifiedBurnins(ffmpeg_burnins.Burnins): self.options_init.update(options_init) def add_text( - self, text, align, frame_start=None, frame_end=None, options=None, cmd="" + self, + text, + align, + frame_start=None, + frame_end=None, + options=None, + cmd="" ): """ Adding static text to a filter. @@ -450,11 +456,13 @@ def burnins_from_data( True by default. Presets must be set separately. Should be dict with 2 keys: - - "options" - sets look of burnins - colors, opacity,...(more info: ModifiedBurnins doc) + - "options" - sets look of burnins - colors, opacity,... + (more info: ModifiedBurnins doc) - *OPTIONAL* default values are used when not included - "burnins" - contains dictionary with burnins settings - *OPTIONAL* burnins won't be added (easier is not to use this) - - each key of "burnins" represents Alignment, there are 6 possibilities: + - each key of "burnins" represents Alignment, + there are 6 possibilities: TOP_LEFT TOP_CENTERED TOP_RIGHT BOTTOM_LEFT BOTTOM_CENTERED BOTTOM_RIGHT - value must be string with text you want to burn-in From 84817a77a33d4e0236ca7cdc9e7300665f77571c Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 22 Mar 2023 15:42:45 +0000 Subject: [PATCH 117/228] Code cosmetics --- .../hosts/maya/plugins/publish/collect_review.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_review.py b/openpype/hosts/maya/plugins/publish/collect_review.py index 0857e8224d..ce11e8a71f 100644 --- a/openpype/hosts/maya/plugins/publish/collect_review.py +++ b/openpype/hosts/maya/plugins/publish/collect_review.py @@ -5,6 +5,7 @@ import pyblish.api from openpype.client import get_subset_by_name from openpype.pipeline import legacy_io +from openpype.hosts.maya.api.lib import get_attribute_input class CollectReview(pyblish.api.InstancePlugin): @@ -146,19 +147,16 @@ class CollectReview(pyblish.api.InstancePlugin): ) # Collect focal length. - #Refactor to lib or use available lib method. - plug = "{0}.focalLength".format(camera) + attr = camera + ".focalLength" focal_length = None - if not cmds.listConnections(plug, destination=False, source=True): - # Static. - focal_length = cmds.getAttr(plug) - else: - # Dynamic. + if get_attribute_input(attr): start = instance.data["frameStart"] end = instance.data["frameEnd"] + 1 focal_length = [ - cmds.getAttr(plug, time=t) for t in range(int(start), int(end)) + cmds.getAttr(attr, time=t) for t in range(int(start), int(end)) ] + else: + focal_length = cmds.getAttr(attr) key = "focalLength" try: From 7ff781f3bddff9b5923789cb3406e67162c43c1a Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 22 Mar 2023 16:32:41 +0000 Subject: [PATCH 118/228] Clean up temporary files and support float truncating --- openpype/scripts/otio_burnin.py | 89 +++++++++++++++++---------------- 1 file changed, 47 insertions(+), 42 deletions(-) diff --git a/openpype/scripts/otio_burnin.py b/openpype/scripts/otio_burnin.py index 4bbb684deb..afae4f9e08 100644 --- a/openpype/scripts/otio_burnin.py +++ b/openpype/scripts/otio_burnin.py @@ -41,9 +41,8 @@ TIMECODE_KEY = "{timecode}" SOURCE_TIMECODE_KEY = "{source_timecode}" -def convert_list_to_cmd(list_to_convert, fps, label=""): +def convert_list_to_commands(list_to_convert, fps, label=""): path = None - #need to clean up temp file when done with tempfile.NamedTemporaryFile(mode="w", delete=False) as f: for i, value in enumerate(list_to_convert): seconds = i / fps @@ -63,10 +62,8 @@ def convert_list_to_cmd(list_to_convert, fps, label=""): f.write(line) f.flush() path = f.name - path = path.replace("\\", "/") - path = path.replace(":", "\\:") - return "sendcmd=f='{}'".format(path) + return path def _get_ffprobe_data(source): @@ -541,6 +538,7 @@ def burnins_from_data( if source_timecode is not None: data[SOURCE_TIMECODE_KEY[1:-1]] = SOURCE_TIMECODE_KEY + clean_up_paths = [] for align_text, value in burnin_values.items(): if not value: continue @@ -583,8 +581,48 @@ def burnins_from_data( print("Source does not have set timecode value.") value = value.replace(SOURCE_TIMECODE_KEY, MISSING_KEY_VALUE) - key_pattern = re.compile(r"(\{.*?[^{0]*\})") + # Convert lists. + cmd = "" + text = None + keys = [i[1] for i in Formatter().parse(value) if i[1] is not None] + list_to_convert = [] + # Warn about nested dictionary support for lists. Ei. we dont support + # it. + if "[" in "".join(keys): + print( + "We dont support converting nested dictionaries to lists," + " so skipping {}".format(value) + ) + else: + for key in keys: + data_value = data[key] + + # Multiple lists are not supported. + if isinstance(data_value, list) and list_to_convert: + raise ValueError( + "Found multiple lists to convert, which is not " + "supported: {}".format(value) + ) + + if isinstance(data_value, list): + print("Found list to convert: {}".format(data_value)) + for v in data_value: + data[key] = v + list_to_convert.append(value.format(**data)) + + if list_to_convert: + value = list_to_convert[0] + path = convert_list_to_commands( + list_to_convert, data["fps"], label=align + ) + cmd = "sendcmd=f='{}'".format(path) + cmd = cmd.replace("\\", "/") + cmd = cmd.replace(":", "\\:") + clean_up_paths.append(path) + + # Failsafe for missing keys. + key_pattern = re.compile(r"(\{.*?[^{0]*\})") missing_keys = [] for group in key_pattern.findall(value): try: @@ -617,42 +655,7 @@ def burnins_from_data( burnin.add_timecode(*args) continue - cmd = "" - text = None - keys = [i[1] for i in Formatter().parse(value) if i[1] is not None] - list_to_convert = [] - - # Warn about nested dictionary support for lists. Ei. we dont support - # it. - if "[" in "".join(keys): - print( - "We dont support converting nested dictionaries to lists," - " so skipping {}".format(value) - ) - else: - for key in keys: - data_value = data[key] - - # Multiple lists are not supported. - if isinstance(data_value, list) and list_to_convert: - raise ValueError( - "Found multiple lists to convert, which is not " - "supported: {}".format(value) - ) - - if isinstance(data_value, list): - print("Found list to convert: {}".format(data_value)) - for v in data_value: - data[key] = v - list_to_convert.append(value.format(**data)) - - if list_to_convert: - text = list_to_convert[0] - cmd = convert_list_to_cmd( - list_to_convert, data["fps"], label=align - ) - else: - text = value.format(**data) + text = value.format(**data) burnin.add_text(text, align, frame_start, frame_end, cmd=cmd) @@ -685,6 +688,8 @@ def burnins_from_data( burnin.render( output_path, args=ffmpeg_args_str, overwrite=overwrite, **data ) + for path in clean_up_paths: + os.remove(path) if __name__ == "__main__": From 833e48ae0f5f31e57b920950a0f5cea38a967263 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 22 Mar 2023 16:43:56 +0000 Subject: [PATCH 119/228] Docs --- website/docs/pype2/admin_presets_plugins.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/website/docs/pype2/admin_presets_plugins.md b/website/docs/pype2/admin_presets_plugins.md index fcdc09439b..2a30e7e8e9 100644 --- a/website/docs/pype2/admin_presets_plugins.md +++ b/website/docs/pype2/admin_presets_plugins.md @@ -293,6 +293,7 @@ If source representation has suffix **"h264"** and burnin suffix is **"client"** - It is allowed to use [Anatomy templates](admin_config#anatomy) themselves in burnins if they can be filled with available data. - Additional keys in burnins: + | Burnin key | Description | | --- | --- | | frame_start | First frame number. | @@ -303,6 +304,7 @@ If source representation has suffix **"h264"** and burnin suffix is **"client"** | resolution_height | Resolution height. | | fps | Fps of an output. | | timecode | Timecode by frame start and fps. | + | focalLength | **Only available in Maya**

    Camera focal length per frame. Use syntax `{focalLength:.2f}` for decimal truncating. Eg. `35.234985` with `{focalLength:.2f}` would produce `35.23`, whereas `{focalLength:.0f}` would produce `35`. | :::warning `timecode` is specific key that can be **only at the end of content**. (`"BOTTOM_RIGHT": "TC: {timecode}"`) From 62ced23858534c081c4aee84714b236507e64439 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 23 Mar 2023 00:45:58 +0800 Subject: [PATCH 120/228] remove irrelevant attrs of data --- openpype/hosts/max/api/lib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index aa7f72b26c..ac7d75db08 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -186,9 +186,9 @@ def reset_scene_resolution(): None """ data = ["data.resolutionWidth", "data.resolutionHeight"] - project_resolution = get_current_project(fields=data)["data"] + project_resolution = get_current_project(fields=data) project_resolution_data = project_resolution["data"] - asset_resolution = get_current_project_asset(fields=data)["data"] + asset_resolution = get_current_project_asset(fields=data) asset_resolution_data = asset_resolution["data"] # Set project resolution project_width = int(project_resolution_data.get("resolutionWidth", 1920)) From 499351a2807e58ed8bf770b63566b2b34e6d8fae Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 23 Mar 2023 00:47:18 +0800 Subject: [PATCH 121/228] remove irrelevant attributes --- openpype/hosts/max/api/lib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index ac1d158792..39657a2525 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -187,9 +187,9 @@ def reset_scene_resolution(): None """ data = ["data.resolutionWidth", "data.resolutionHeight"] - project_resolution = get_current_project(fields=data)["data"] + project_resolution = get_current_project(fields=data) project_resolution_data = project_resolution["data"] - asset_resolution = get_current_project_asset(fields=data)["data"] + asset_resolution = get_current_project_asset(fields=data) asset_resolution_data = asset_resolution["data"] # Set project resolution project_width = int(project_resolution_data.get("resolutionWidth", 1920)) From 5995750ec1eff10ad5265b60ce75dfb1b72a8262 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 23 Mar 2023 09:14:58 +0000 Subject: [PATCH 122/228] Functionality --- openpype/hosts/maya/plugins/create/create_review.py | 2 ++ openpype/hosts/maya/plugins/publish/extract_playblast.py | 5 ++++- openpype/hosts/maya/plugins/publish/extract_thumbnail.py | 5 ++++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_review.py b/openpype/hosts/maya/plugins/create/create_review.py index f1b626c06b..e709239ae7 100644 --- a/openpype/hosts/maya/plugins/create/create_review.py +++ b/openpype/hosts/maya/plugins/create/create_review.py @@ -26,6 +26,7 @@ class CreateReview(plugin.Creator): "alpha cut" ] useMayaTimeline = True + panZoom = False def __init__(self, *args, **kwargs): super(CreateReview, self).__init__(*args, **kwargs) @@ -45,5 +46,6 @@ class CreateReview(plugin.Creator): data["keepImages"] = self.keepImages data["imagePlane"] = self.imagePlane data["transparency"] = self.transparency + data["panZoom"] = self.panZoom self.data = data diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 94571ff731..31cb3435ac 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -114,7 +114,10 @@ class ExtractPlayblast(publish.Extractor): # Disable Pan/Zoom. pan_zoom = cmds.getAttr("{}.panZoomEnabled".format(preset["camera"])) - cmds.setAttr("{}.panZoomEnabled".format(preset["camera"]), False) + cmds.setAttr( + "{}.panZoomEnabled".format(preset["camera"]), + instance.data["panZoom"] + ) # Need to explicitly enable some viewport changes so the viewport is # refreshed ahead of playblasting. diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index 1d94bd58c5..ba8712c73d 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -124,7 +124,10 @@ class ExtractThumbnail(publish.Extractor): # Disable Pan/Zoom. pan_zoom = cmds.getAttr("{}.panZoomEnabled".format(preset["camera"])) - cmds.setAttr("{}.panZoomEnabled".format(preset["camera"]), False) + cmds.setAttr( + "{}.panZoomEnabled".format(preset["camera"]), + instance.data["panZoom"] + ) with lib.maintained_time(): # Force viewer to False in call to capture because we have our own From d791000b80cb0b4b05e4d0a4d04edafcf44f65e5 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 23 Mar 2023 09:25:15 +0000 Subject: [PATCH 123/228] Settings --- openpype/settings/defaults/project_settings/maya.json | 3 ++- .../schemas/projects_schema/schemas/schema_maya_capture.json | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 2aa95fd1be..4a97ea8a09 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -817,7 +817,8 @@ }, "Generic": { "isolate_view": true, - "off_screen": true + "off_screen": true, + "pan_zoom": false }, "Renderer": { "rendererName": "vp2Renderer" diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json index 1f0e4eeffb..8d37aa299e 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json @@ -91,6 +91,11 @@ "type": "boolean", "key": "off_screen", "label": " Off Screen" + }, + { + "type": "boolean", + "key": "pan_zoom", + "label": " 2D Pan/Zoom" } ] }, From 21c25ac21c3d362c6f602a30706a3c480ab46620 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 23 Mar 2023 09:25:22 +0000 Subject: [PATCH 124/228] Code cosmetics --- .../maya/plugins/publish/extract_thumbnail.py | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index ba8712c73d..2d3daa53c6 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -26,28 +26,28 @@ class ExtractThumbnail(publish.Extractor): def process(self, instance): self.log.info("Extracting capture..") - camera = instance.data['review_camera'] + camera = instance.data["review_camera"] - capture_preset = ( - instance.context.data["project_settings"]['maya']['publish']['ExtractPlayblast']['capture_preset'] - ) + maya_setting = instance.context.data["project_settings"]["maya"] + plugin_setting = maya_setting["publish"]["ExtractPlayblast"] + capture_preset = plugin_setting["capture_preset"] override_viewport_options = ( - capture_preset['Viewport Options']['override_viewport_options'] + capture_preset["Viewport Options"]["override_viewport_options"] ) try: preset = lib.load_capture_preset(data=capture_preset) except KeyError as ke: - self.log.error('Error loading capture presets: {}'.format(str(ke))) + self.log.error("Error loading capture presets: {}".format(str(ke))) preset = {} - self.log.info('Using viewport preset: {}'.format(preset)) + self.log.info("Using viewport preset: {}".format(preset)) # preset["off_screen"] = False - preset['camera'] = camera - preset['start_frame'] = instance.data["frameStart"] - preset['end_frame'] = instance.data["frameStart"] - preset['camera_options'] = { + preset["camera"] = camera + preset["start_frame"] = instance.data["frameStart"] + preset["end_frame"] = instance.data["frameStart"] + preset["camera_options"] = { "displayGateMask": False, "displayResolution": False, "displayFilmGate": False, @@ -74,14 +74,14 @@ class ExtractThumbnail(publish.Extractor): # used, if not then the asset resolution is # used if review_instance_width and review_instance_height: - preset['width'] = review_instance_width - preset['height'] = review_instance_height + preset["width"] = review_instance_width + preset["height"] = review_instance_height elif width_preset and height_preset: - preset['width'] = width_preset - preset['height'] = height_preset + preset["width"] = width_preset + preset["height"] = height_preset elif asset_width and asset_height: - preset['width'] = asset_width - preset['height'] = asset_height + preset["width"] = asset_width + preset["height"] = asset_height # Create temp directory for thumbnail # - this is to avoid "override" of source file @@ -96,8 +96,8 @@ class ExtractThumbnail(publish.Extractor): self.log.info("Outputting images to %s" % path) - preset['filename'] = path - preset['overwrite'] = True + preset["filename"] = path + preset["overwrite"] = True pm.refresh(f=True) @@ -133,7 +133,7 @@ class ExtractThumbnail(publish.Extractor): # Force viewer to False in call to capture because we have our own # viewer opening call to allow a signal to trigger between # playblast and viewer - preset['viewer'] = False + preset["viewer"] = False # Update preset with current panel setting # if override_viewport_options is turned off @@ -156,9 +156,9 @@ class ExtractThumbnail(publish.Extractor): instance.data["representations"] = [] representation = { - 'name': 'thumbnail', - 'ext': 'jpg', - 'files': thumbnail, + "name": "thumbnail", + "ext": "jpg", + "files": thumbnail, "stagingDir": dst_staging, "thumbnail": True } From 75e67807c3c7af4c68e3fcebd5b155fd4b5f51fe Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 23 Mar 2023 10:42:54 +0000 Subject: [PATCH 125/228] Docs --- .../schemas/schema_maya_capture.json | 2 +- website/docs/admin_hosts_maya.md | 33 ++++++++++++++++++ .../maya-admin_extract_playblast_settings.png | Bin 0 -> 26814 bytes ...ract_playblast_settings_camera_options.png | Bin 0 -> 16732 bytes ...ct_playblast_settings_viewport_options.png | Bin 0 -> 1064191 bytes 5 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 website/docs/assets/maya-admin_extract_playblast_settings.png create mode 100644 website/docs/assets/maya-admin_extract_playblast_settings_camera_options.png create mode 100644 website/docs/assets/maya-admin_extract_playblast_settings_viewport_options.png diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json index 8d37aa299e..dec5a5cdc2 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json @@ -161,7 +161,7 @@ { "type": "boolean", "key": "override_viewport_options", - "label": "override_viewport_options" + "label": "Override Viewport Options" }, { "type": "enum", diff --git a/website/docs/admin_hosts_maya.md b/website/docs/admin_hosts_maya.md index 23cacb4193..fe30703c2e 100644 --- a/website/docs/admin_hosts_maya.md +++ b/website/docs/admin_hosts_maya.md @@ -110,6 +110,39 @@ or Deadlines **Draft Tile Assembler**. This is useful to fix some specific renderer glitches and advanced hacking of Maya Scene files. `Patch name` is label for patch for easier orientation. `Patch regex` is regex used to find line in file, after `Patch line` string is inserted. Note that you need to add line ending. +### Extract Playblast Settings (review) +These settings provide granular control over how the playblasts or reviews are produced in Maya. + +![Extract Playblast Settings](assets/maya-admin_extract_playblast_settings.png) + +- **Compression type** which file encoding to use. +- **Data format** what format is the file encoding. +- **Quality** lets you control the compression value for the output. Results can vary depending on the compression you selected. Quality values can range from 0 to 100, with a default value of 95. +- **Background Color** the viewports background color. +- **Background Bottom** the viewports background bottom color. +- **Background Top** the viewports background top color. +- **Override display options** override the viewports display options to use what is set in the settings. +- **Isolate view** isolates the view to what is in the review instance. If only a camera is present in the review instance, all nodes are displayed in view. +- **Off Screen** records the playblast hidden from the user. +- **2D Pan/Zoom** enables the 2D Pan/Zoom functionality of the camera. +- **Renderer name** which renderer to use for playblasting. +- **Width** width of the output resolution. +- **Height** height of the output resolution. + +#### Viewport Options + +Most settings to override in the viewport are self explanatory and can be found in Maya. + +![Extract Playblast Settings](assets/maya-admin_extract_playblast_settings_viewport_options.png) + +- **Override Viewport Options** enable to use the settings below for the viewport when publishing the review. + +#### Camera Options + +These camera options can be overridden when publishing the review. They can be found on the camera shape node. + +![Extract Playblast Settings](assets/maya-admin_extract_playblast_settings_camera_options.png) + ## Custom Menu You can add your custom tools menu into Maya by extending definitions in **Maya -> Scripts Menu Definition**. ![Custom menu definition](assets/maya-admin_scriptsmenu.png) diff --git a/website/docs/assets/maya-admin_extract_playblast_settings.png b/website/docs/assets/maya-admin_extract_playblast_settings.png new file mode 100644 index 0000000000000000000000000000000000000000..9312e30321b332c1d22c18f1c77175ca25cf954e GIT binary patch literal 26814 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

    KZ;uX*}2 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

    _?B@ssfQYlBAHF=3EyzE&_=Z2gg)O{M_nh8~ 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�lF z3v}%-a*KhmUwz_Wgu1>Q(BJ-w9?PAou;fxSU`r9$p#_)iA<@r11Tvsk)OhOOfdI0Z z#t8BoE41vKMk^dL(+B61@;Ps0K^LtwC%Qnxh*D{7FuMRXaWNnAMsmyxutmZlJ?DF{ zPu7pMq^VgMYZBGW(;p9{az9d}pm4*0Aq7Byh9l)m%gVkQ-987;Wdh;0fYE2WaMJp` z(f4nxvwf2s7oY~pTS?W!Z&BwY>uPF2QF{6~AN;tE*>QPVuub`$JlNd>^$z$@zm9KP zFRIywgDN(ypGGW{g-M3x=aFB_ORy&BY`V`RLJt<>OL+T*Y5r6cPv8^^ezTTIf^+=m zyQ>CkvfZu%Xr6(!?UgF(uW~_`dK%vLbl;I_2_Bfayd1Ms>kv>xDTz4xk7CF4R`k}vS<3$p9{;7_0sHH(bpB4e|G%Rc fw&7b=Cmv8OJ)uU~wvf`no{Qp&`t?g-pX>iGR($zU literal 0 HcmV?d00001 diff --git a/website/docs/assets/maya-admin_extract_playblast_settings_camera_options.png b/website/docs/assets/maya-admin_extract_playblast_settings_camera_options.png new file mode 100644 index 0000000000000000000000000000000000000000..1a5561328e056fe03278f78c56ea6b837a384e56 GIT binary patch literal 16732 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 literal 0 HcmV?d00001 diff --git a/website/docs/assets/maya-admin_extract_playblast_settings_viewport_options.png b/website/docs/assets/maya-admin_extract_playblast_settings_viewport_options.png new file mode 100644 index 0000000000000000000000000000000000000000..5b801775d32338ea9dcb051d2b01a008697c7bd8 GIT binary patch literal 1064191 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%0AJpOqGmRpk29oF=8t0UWz?Ldih)A1?YQ79f+ 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 W{e?~d Date: Thu, 23 Mar 2023 10:43:27 +0000 Subject: [PATCH 126/228] Fix representation hardcoded to png --- openpype/hosts/maya/plugins/publish/extract_playblast.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 31cb3435ac..f8bc6987c6 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -207,9 +207,9 @@ class ExtractPlayblast(publish.Extractor): collected_files = collected_files[0] representation = { - 'name': 'png', - 'ext': 'png', - 'files': collected_files, + "name": self.capture_preset["Codec"]["compression"], + "ext": self.capture_preset["Codec"]["compression"], + "files": collected_files, "stagingDir": stagingdir, "frameStart": start, "frameEnd": end, From 6eeed5d8e72031acb1b7238cc04083821c633262 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 23 Mar 2023 10:43:36 +0000 Subject: [PATCH 127/228] Code cosmetics --- .../maya/plugins/publish/extract_playblast.py | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index f8bc6987c6..ab1e1a65c6 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -43,7 +43,7 @@ class ExtractPlayblast(publish.Extractor): self.log.info("start: {}, end: {}".format(start, end)) # get cameras - camera = instance.data['review_camera'] + camera = instance.data["review_camera"] preset = lib.load_capture_preset(data=self.capture_preset) # Grab capture presets from the project settings @@ -57,23 +57,23 @@ class ExtractPlayblast(publish.Extractor): asset_height = asset_data.get("resolutionHeight") review_instance_width = instance.data.get("review_width") review_instance_height = instance.data.get("review_height") - preset['camera'] = camera + preset["camera"] = camera # Tests if project resolution is set, # if it is a value other than zero, that value is # used, if not then the asset resolution is # used if review_instance_width and review_instance_height: - preset['width'] = review_instance_width - preset['height'] = review_instance_height + preset["width"] = review_instance_width + preset["height"] = review_instance_height elif width_preset and height_preset: - preset['width'] = width_preset - preset['height'] = height_preset + preset["width"] = width_preset + preset["height"] = height_preset elif asset_width and asset_height: - preset['width'] = asset_width - preset['height'] = asset_height - preset['start_frame'] = start - preset['end_frame'] = end + preset["width"] = asset_width + preset["height"] = asset_height + preset["start_frame"] = start + preset["end_frame"] = end # Enforce persisting camera depth of field camera_options = preset.setdefault("camera_options", {}) @@ -86,8 +86,8 @@ class ExtractPlayblast(publish.Extractor): self.log.info("Outputting images to %s" % path) - preset['filename'] = path - preset['overwrite'] = True + preset["filename"] = path + preset["overwrite"] = True pm.refresh(f=True) @@ -139,7 +139,7 @@ class ExtractPlayblast(publish.Extractor): ) override_viewport_options = ( - capture_presets['Viewport Options']['override_viewport_options'] + capture_presets["Viewport Options"]["override_viewport_options"] ) with lib.maintained_time(): filename = preset.get("filename", "%TEMP%") @@ -147,7 +147,7 @@ class ExtractPlayblast(publish.Extractor): # Force viewer to False in call to capture because we have our own # viewer opening call to allow a signal to trigger between # playblast and viewer - preset['viewer'] = False + preset["viewer"] = False # Update preset with current panel setting # if override_viewport_options is turned off @@ -183,7 +183,7 @@ class ExtractPlayblast(publish.Extractor): self.log.debug("filename {}".format(filename)) frame_collection = None for collection in collections: - filebase = collection.format('{head}').rstrip(".") + filebase = collection.format("{head}").rstrip(".") self.log.debug("collection head {}".format(filebase)) if filebase in filename: frame_collection = collection @@ -213,9 +213,9 @@ class ExtractPlayblast(publish.Extractor): "stagingDir": stagingdir, "frameStart": start, "frameEnd": end, - 'fps': fps, - 'preview': True, - 'tags': tags, - 'camera_name': camera_node_name + "fps": fps, + "preview": True, + "tags": tags, + "camera_name": camera_node_name } instance.data["representations"].append(representation) From 5197aa2ce7737dd15aa6941515a4fc0669edd046 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 23 Mar 2023 11:51:49 +0000 Subject: [PATCH 128/228] Suggestion to change labels. --- .../schemas/projects_schema/schemas/schema_maya_capture.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json index 1f0e4eeffb..cec4b6eb90 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json @@ -19,12 +19,12 @@ { "type": "text", "key": "compression", - "label": "Compression type" + "label": "Encoding" }, { "type": "text", "key": "format", - "label": "Data format" + "label": "Format" }, { "type": "number", From 54d566a4ee76924a2d091d4c3fbf7e0f076749df Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Thu, 23 Mar 2023 12:17:02 +0000 Subject: [PATCH 129/228] Update website/docs/admin_hosts_maya.md Co-authored-by: Roy Nieterau --- website/docs/admin_hosts_maya.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/admin_hosts_maya.md b/website/docs/admin_hosts_maya.md index fe30703c2e..716bbadd88 100644 --- a/website/docs/admin_hosts_maya.md +++ b/website/docs/admin_hosts_maya.md @@ -139,7 +139,7 @@ Most settings to override in the viewport are self explanatory and can be found #### Camera Options -These camera options can be overridden when publishing the review. They can be found on the camera shape node. +These options are set on the camera shape when publishing the review. They correspond to attributes on the Maya camera shape node. ![Extract Playblast Settings](assets/maya-admin_extract_playblast_settings_camera_options.png) From cc7a9f32c1efd58d89f18f0da4101d9df44666c6 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 23 Mar 2023 14:30:51 +0100 Subject: [PATCH 130/228] :art: add OP startup script for max to cmdline args --- openpype/settings/defaults/system_settings/applications.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index 5fd9b926fb..eb3a88ce66 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -133,7 +133,7 @@ "linux": [] }, "arguments": { - "windows": [], + "windows": ["-U MAXScript {OPENPYPE_ROOT}\\openpype\\hosts\\max\\startup\\startup.ms"], "darwin": [], "linux": [] }, From 93ad644f7488dcb0d10e6183618a85b0016850d3 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 23 Mar 2023 15:36:04 +0000 Subject: [PATCH 131/228] Use pan zoom in capture instead of collect/restore. --- openpype/hosts/maya/plugins/publish/extract_playblast.py | 9 ++------- openpype/hosts/maya/plugins/publish/extract_thumbnail.py | 9 ++------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index ab1e1a65c6..bdb34474e8 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -113,11 +113,8 @@ class ExtractPlayblast(publish.Extractor): preset["viewport_options"] = {"imagePlane": image_plane} # Disable Pan/Zoom. - pan_zoom = cmds.getAttr("{}.panZoomEnabled".format(preset["camera"])) - cmds.setAttr( - "{}.panZoomEnabled".format(preset["camera"]), - instance.data["panZoom"] - ) + preset.pop("pan_zoom", None) + preset["camera_options"]["panZoomEnabled"] = instance.data["panZoom"] # Need to explicitly enable some viewport changes so the viewport is # refreshed ahead of playblasting. @@ -170,8 +167,6 @@ class ExtractPlayblast(publish.Extractor): instance.data["panel"], edit=True, **viewport_defaults ) - cmds.setAttr("{}.panZoomEnabled".format(preset["camera"]), pan_zoom) - self.log.debug("playblast path {}".format(path)) collected_files = os.listdir(stagingdir) diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index 2d3daa53c6..f2d084b828 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -123,11 +123,8 @@ class ExtractThumbnail(publish.Extractor): preset["viewport_options"] = {"imagePlane": image_plane} # Disable Pan/Zoom. - pan_zoom = cmds.getAttr("{}.panZoomEnabled".format(preset["camera"])) - cmds.setAttr( - "{}.panZoomEnabled".format(preset["camera"]), - instance.data["panZoom"] - ) + preset.pop("pan_zoom", None) + preset["camera_options"]["panZoomEnabled"] = instance.data["panZoom"] with lib.maintained_time(): # Force viewer to False in call to capture because we have our own @@ -148,8 +145,6 @@ class ExtractThumbnail(publish.Extractor): _, thumbnail = os.path.split(playblast) - cmds.setAttr("{}.panZoomEnabled".format(preset["camera"]), pan_zoom) - self.log.info("file list {}".format(thumbnail)) if "representations" not in instance.data: From e7b2e0026f2769e5b1100af1a72e260f17404b65 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 23 Mar 2023 15:36:57 +0000 Subject: [PATCH 132/228] BigRoy feedback --- website/docs/admin_hosts_maya.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/website/docs/admin_hosts_maya.md b/website/docs/admin_hosts_maya.md index fe30703c2e..6271feda15 100644 --- a/website/docs/admin_hosts_maya.md +++ b/website/docs/admin_hosts_maya.md @@ -113,6 +113,8 @@ This is useful to fix some specific renderer glitches and advanced hacking of Ma ### Extract Playblast Settings (review) These settings provide granular control over how the playblasts or reviews are produced in Maya. +Some of these settings are also available on the instance itself, in which case these settings will become the default value when creating the review instance. + ![Extract Playblast Settings](assets/maya-admin_extract_playblast_settings.png) - **Compression type** which file encoding to use. @@ -126,8 +128,8 @@ These settings provide granular control over how the playblasts or reviews are p - **Off Screen** records the playblast hidden from the user. - **2D Pan/Zoom** enables the 2D Pan/Zoom functionality of the camera. - **Renderer name** which renderer to use for playblasting. -- **Width** width of the output resolution. -- **Height** height of the output resolution. +- **Width** width of the output resolution. If this value is `0`, the asset's width is used. +- **Height** height of the output resolution. If this value is `0`, the asset's height is used. #### Viewport Options From d88cd48b8eda227873191b830a5792f3403b1abf Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 24 Mar 2023 07:48:28 +0000 Subject: [PATCH 133/228] Refactor to nested/stacked contextlib --- .../maya/plugins/publish/extract_playblast.py | 83 ++++++++++++------- 1 file changed, 52 insertions(+), 31 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index a652db2eb5..9d35789343 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -1,5 +1,6 @@ import os import json +import contextlib import clique import capture @@ -11,6 +12,16 @@ from maya import cmds import pymel.core as pm +@contextlib.contextmanager +def panel_camera(panel, camera): + original_camera = cmds.modelPanel(panel, query=True, camera=True) + try: + cmds.modelPanel(panel, edit=True, camera=camera) + yield + finally: + cmds.modelPanel(panel, edit=True, camera=original_camera) + + class ExtractPlayblast(publish.Extractor): """Extract viewport playblast. @@ -25,6 +36,31 @@ class ExtractPlayblast(publish.Extractor): optional = True capture_preset = {} + def _capture(self, preset, override_viewport_options, instance): + filename = preset.get("filename", "%TEMP%") + + # Force viewer to False in call to capture because we have our own + # viewer opening call to allow a signal to trigger between + # playblast and viewer + preset['viewer'] = False + + # Update preset with current panel setting + # if override_viewport_options is turned off + if not override_viewport_options: + panel_preset = capture.parse_view(instance.data["panel"]) + panel_preset.pop("camera") + preset.update(panel_preset) + + self.log.info( + "Using preset:\n{}".format( + json.dumps(preset, sort_keys=True, indent=4) + ) + ) + + path = capture.capture(log=self.log, **preset) + + return filename, path + def process(self, instance): self.log.info("Extracting capture..") @@ -112,15 +148,6 @@ class ExtractPlayblast(publish.Extractor): else: preset["viewport_options"] = {"imagePlane": image_plane} - # Image planes do not update the file sequence unless the active panel - # is viewing through the camera. - panel_camera = cmds.modelPanel( - instance.data["panel"], query=True, camera=True - ) - cmds.modelPanel( - instance.data["panel"], edit=True, camera=preset["camera"] - ) - # Disable Pan/Zoom. pan_zoom = cmds.getAttr("{}.panZoomEnabled".format(preset["camera"])) cmds.setAttr("{}.panZoomEnabled".format(preset["camera"]), False) @@ -147,28 +174,25 @@ class ExtractPlayblast(publish.Extractor): override_viewport_options = ( capture_presets['Viewport Options']['override_viewport_options'] ) - with lib.maintained_time(): - filename = preset.get("filename", "%TEMP%") - # Force viewer to False in call to capture because we have our own - # viewer opening call to allow a signal to trigger between - # playblast and viewer - preset['viewer'] = False - - # Update preset with current panel setting - # if override_viewport_options is turned off - if not override_viewport_options: - panel_preset = capture.parse_view(instance.data["panel"]) - panel_preset.pop("camera") - preset.update(panel_preset) - - self.log.info( - "Using preset:\n{}".format( - json.dumps(preset, sort_keys=True, indent=4) + if getattr(contextlib, "nested", None): + with contextlib.nested( + lib.maintained_time(), + panel_camera(instance.data["panel"], preset["camera"]) + ): + filename, path = self._capture( + preset, override_viewport_options, instance + ) + else: + with contextlib.ExitStack() as stack: + stack.enter_context(lib.maintained_time()) + stack.enter_context( + panel_camera(instance.data["panel"], preset["camera"]) ) - ) - path = capture.capture(log=self.log, **preset) + filename, path = self._capture( + preset, override_viewport_options, instance + ) # Restoring viewport options. if viewport_defaults: @@ -178,9 +202,6 @@ class ExtractPlayblast(publish.Extractor): cmds.setAttr("{}.panZoomEnabled".format(preset["camera"]), pan_zoom) - # Restore panel camera. - cmds.modelPanel(instance.data["panel"], edit=True, camera=panel_camera) - self.log.debug("playblast path {}".format(path)) collected_files = os.listdir(stagingdir) From 4d42e58074c873f2247fa31e4d86b51c6c1b4262 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Fri, 24 Mar 2023 09:37:05 +0000 Subject: [PATCH 134/228] Update openpype/hosts/maya/plugins/publish/extract_playblast.py --- openpype/hosts/maya/plugins/publish/extract_playblast.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 9d35789343..e7dc42d2c0 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -175,7 +175,10 @@ class ExtractPlayblast(publish.Extractor): capture_presets['Viewport Options']['override_viewport_options'] ) + # Need to ensure Python 2 compatibility. + # TODO: Remove once dropping Python 2. if getattr(contextlib, "nested", None): + # Python 3 compatibility. with contextlib.nested( lib.maintained_time(), panel_camera(instance.data["panel"], preset["camera"]) @@ -184,6 +187,7 @@ class ExtractPlayblast(publish.Extractor): preset, override_viewport_options, instance ) else: + # Python 2 compatibility. with contextlib.ExitStack() as stack: stack.enter_context(lib.maintained_time()) stack.enter_context( From d1038ace686dea66069319b2a8e0df7df3c4e4c7 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 24 Mar 2023 09:44:39 +0000 Subject: [PATCH 135/228] Refactor _capture --- .../maya/plugins/publish/extract_playblast.py | 35 +++++++++---------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index e7dc42d2c0..9bd859ade1 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -37,29 +37,13 @@ class ExtractPlayblast(publish.Extractor): capture_preset = {} def _capture(self, preset, override_viewport_options, instance): - filename = preset.get("filename", "%TEMP%") - - # Force viewer to False in call to capture because we have our own - # viewer opening call to allow a signal to trigger between - # playblast and viewer - preset['viewer'] = False - - # Update preset with current panel setting - # if override_viewport_options is turned off - if not override_viewport_options: - panel_preset = capture.parse_view(instance.data["panel"]) - panel_preset.pop("camera") - preset.update(panel_preset) - self.log.info( "Using preset:\n{}".format( json.dumps(preset, sort_keys=True, indent=4) ) ) - path = capture.capture(log=self.log, **preset) - - return filename, path + return capture.capture(log=self.log, **preset) def process(self, instance): self.log.info("Extracting capture..") @@ -175,6 +159,18 @@ class ExtractPlayblast(publish.Extractor): capture_presets['Viewport Options']['override_viewport_options'] ) + # Force viewer to False in call to capture because we have our own + # viewer opening call to allow a signal to trigger between + # playblast and viewer + preset['viewer'] = False + + # Update preset with current panel setting + # if override_viewport_options is turned off + if not override_viewport_options: + panel_preset = capture.parse_view(instance.data["panel"]) + panel_preset.pop("camera") + preset.update(panel_preset) + # Need to ensure Python 2 compatibility. # TODO: Remove once dropping Python 2. if getattr(contextlib, "nested", None): @@ -183,7 +179,7 @@ class ExtractPlayblast(publish.Extractor): lib.maintained_time(), panel_camera(instance.data["panel"], preset["camera"]) ): - filename, path = self._capture( + path = self._capture( preset, override_viewport_options, instance ) else: @@ -194,7 +190,7 @@ class ExtractPlayblast(publish.Extractor): panel_camera(instance.data["panel"], preset["camera"]) ) - filename, path = self._capture( + path = self._capture( preset, override_viewport_options, instance ) @@ -214,6 +210,7 @@ class ExtractPlayblast(publish.Extractor): minimum_items=1, patterns=patterns) + filename = preset.get("filename", "%TEMP%") self.log.debug("filename {}".format(filename)) frame_collection = None for collection in collections: From 799a6b6cc457d4ef6b4b28f18fe6c831521376ea Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 24 Mar 2023 10:02:48 +0000 Subject: [PATCH 136/228] Remove path variable and _capture arguments. --- .../maya/plugins/publish/extract_playblast.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 9bd859ade1..41e4f3a7c2 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -36,14 +36,15 @@ class ExtractPlayblast(publish.Extractor): optional = True capture_preset = {} - def _capture(self, preset, override_viewport_options, instance): + def _capture(self, preset): self.log.info( "Using preset:\n{}".format( json.dumps(preset, sort_keys=True, indent=4) ) ) - return capture.capture(log=self.log, **preset) + path = capture.capture(log=self.log, **preset) + self.log.debug("playblast path {}".format(path)) def process(self, instance): self.log.info("Extracting capture..") @@ -179,9 +180,7 @@ class ExtractPlayblast(publish.Extractor): lib.maintained_time(), panel_camera(instance.data["panel"], preset["camera"]) ): - path = self._capture( - preset, override_viewport_options, instance - ) + self._capture(preset) else: # Python 2 compatibility. with contextlib.ExitStack() as stack: @@ -190,9 +189,7 @@ class ExtractPlayblast(publish.Extractor): panel_camera(instance.data["panel"], preset["camera"]) ) - path = self._capture( - preset, override_viewport_options, instance - ) + self._capture(preset) # Restoring viewport options. if viewport_defaults: @@ -202,8 +199,6 @@ class ExtractPlayblast(publish.Extractor): cmds.setAttr("{}.panZoomEnabled".format(preset["camera"]), pan_zoom) - self.log.debug("playblast path {}".format(path)) - collected_files = os.listdir(stagingdir) patterns = [clique.PATTERNS["frames"]] collections, remainder = clique.assemble(collected_files, From de4b3e4d659e38715a78290fc1c05e00fedb7432 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 24 Mar 2023 15:25:49 +0100 Subject: [PATCH 137/228] Ftrack: Hierarchical <> Non-Hierarchical attributes sync fix (#4635) * modify action to use 'CustomAttributeValue' and fix bugs * modify and fix event handler to push hierarchical values * added few smaller comments * removed unused variables --- .../action_push_frame_values_to_task.py | 316 +++-- .../event_push_frame_values_to_task.py | 1096 ++++++----------- 2 files changed, 546 insertions(+), 866 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/action_push_frame_values_to_task.py b/openpype/modules/ftrack/event_handlers_server/action_push_frame_values_to_task.py index 1209375f82..a698195c59 100644 --- a/openpype/modules/ftrack/event_handlers_server/action_push_frame_values_to_task.py +++ b/openpype/modules/ftrack/event_handlers_server/action_push_frame_values_to_task.py @@ -9,7 +9,7 @@ from openpype_modules.ftrack.lib import ( class PushHierValuesToNonHier(ServerAction): - """Action push hierarchical custom attribute values to non hierarchical. + """Action push hierarchical custom attribute values to non-hierarchical. Hierarchical value is also pushed to their task entities. @@ -119,17 +119,109 @@ class PushHierValuesToNonHier(ServerAction): self.join_query_keys(object_ids) )).all() - output = {} + attrs_by_obj_id = collections.defaultdict(list) hiearchical = [] for attr in attrs: if attr["is_hierarchical"]: hiearchical.append(attr) continue obj_id = attr["object_type_id"] - if obj_id not in output: - output[obj_id] = [] - output[obj_id].append(attr) - return output, hiearchical + attrs_by_obj_id[obj_id].append(attr) + return attrs_by_obj_id, hiearchical + + def query_attr_value( + self, + session, + hier_attrs, + attrs_by_obj_id, + dst_object_type_ids, + task_entity_ids, + non_task_entity_ids, + parent_id_by_entity_id + ): + all_non_task_ids_with_parents = set() + for entity_id in non_task_entity_ids: + all_non_task_ids_with_parents.add(entity_id) + _entity_id = entity_id + while True: + parent_id = parent_id_by_entity_id.get(_entity_id) + if ( + parent_id is None + or parent_id in all_non_task_ids_with_parents + ): + break + all_non_task_ids_with_parents.add(parent_id) + _entity_id = parent_id + + all_entity_ids = ( + set(all_non_task_ids_with_parents) + | set(task_entity_ids) + ) + attr_ids = {attr["id"] for attr in hier_attrs} + for obj_id in dst_object_type_ids: + attrs = attrs_by_obj_id.get(obj_id) + if attrs is not None: + for attr in attrs: + attr_ids.add(attr["id"]) + + real_values_by_entity_id = { + entity_id: {} + for entity_id in all_entity_ids + } + + attr_values = query_custom_attributes( + session, attr_ids, all_entity_ids, True + ) + for item in attr_values: + entity_id = item["entity_id"] + attr_id = item["configuration_id"] + real_values_by_entity_id[entity_id][attr_id] = item["value"] + + # Fill hierarchical values + hier_attrs_key_by_id = { + hier_attr["id"]: hier_attr + for hier_attr in hier_attrs + } + hier_values_per_entity_id = {} + for entity_id in all_non_task_ids_with_parents: + real_values = real_values_by_entity_id[entity_id] + hier_values_per_entity_id[entity_id] = {} + for attr_id, attr in hier_attrs_key_by_id.items(): + key = attr["key"] + hier_values_per_entity_id[entity_id][key] = ( + real_values.get(attr_id) + ) + + output = {} + for entity_id in non_task_entity_ids: + output[entity_id] = {} + for attr in hier_attrs_key_by_id.values(): + key = attr["key"] + value = hier_values_per_entity_id[entity_id][key] + tried_ids = set() + if value is None: + tried_ids.add(entity_id) + _entity_id = entity_id + while value is None: + parent_id = parent_id_by_entity_id.get(_entity_id) + if not parent_id: + break + value = hier_values_per_entity_id[parent_id][key] + if value is not None: + break + _entity_id = parent_id + tried_ids.add(parent_id) + + if value is None: + value = attr["default"] + + if value is not None: + for ent_id in tried_ids: + hier_values_per_entity_id[ent_id][key] = value + + output[entity_id][key] = value + + return real_values_by_entity_id, output def propagate_values(self, session, event, selected_entities): ftrack_settings = self.get_ftrack_settings( @@ -156,29 +248,24 @@ class PushHierValuesToNonHier(ServerAction): } task_object_type = object_types_by_low_name["task"] - destination_object_types = [task_object_type] + dst_object_type_ids = {task_object_type["id"]} for ent_type in interest_entity_types: obj_type = object_types_by_low_name.get(ent_type) - if obj_type and obj_type not in destination_object_types: - destination_object_types.append(obj_type) - - destination_object_type_ids = set( - obj_type["id"] - for obj_type in destination_object_types - ) + if obj_type: + dst_object_type_ids.add(obj_type["id"]) interest_attributes = action_settings["interest_attributes"] # Find custom attributes definitions attrs_by_obj_id, hier_attrs = self.attrs_configurations( - session, destination_object_type_ids, interest_attributes + session, dst_object_type_ids, interest_attributes ) # Filter destination object types if they have any object specific # custom attribute - for obj_id in tuple(destination_object_type_ids): + for obj_id in tuple(dst_object_type_ids): if obj_id not in attrs_by_obj_id: - destination_object_type_ids.remove(obj_id) + dst_object_type_ids.remove(obj_id) - if not destination_object_type_ids: + if not dst_object_type_ids: # TODO report that there are not matching custom attributes return { "success": True, @@ -192,14 +279,14 @@ class PushHierValuesToNonHier(ServerAction): session, selected_ids, project_entity, - destination_object_type_ids + dst_object_type_ids ) self.log.debug("Preparing whole project hierarchy by ids.") entities_by_obj_id = { obj_id: [] - for obj_id in destination_object_type_ids + for obj_id in dst_object_type_ids } self.log.debug("Filtering Task entities.") @@ -223,10 +310,16 @@ class PushHierValuesToNonHier(ServerAction): "message": "Nothing to do in your selection." } - self.log.debug("Getting Hierarchical custom attribute values parents.") - hier_values_by_entity_id = self.get_hier_values( + self.log.debug("Getting Custom attribute values.") + ( + real_values_by_entity_id, + hier_values_by_entity_id + ) = self.query_attr_value( session, hier_attrs, + attrs_by_obj_id, + dst_object_type_ids, + task_entity_ids, non_task_entity_ids, parent_id_by_entity_id ) @@ -237,7 +330,8 @@ class PushHierValuesToNonHier(ServerAction): hier_attrs, task_entity_ids, hier_values_by_entity_id, - parent_id_by_entity_id + parent_id_by_entity_id, + real_values_by_entity_id ) self.log.debug("Setting values to entities themselves.") @@ -245,7 +339,8 @@ class PushHierValuesToNonHier(ServerAction): session, entities_by_obj_id, attrs_by_obj_id, - hier_values_by_entity_id + hier_values_by_entity_id, + real_values_by_entity_id ) return True @@ -322,112 +417,64 @@ class PushHierValuesToNonHier(ServerAction): return parent_id_by_entity_id, filtered_entities - def get_hier_values( - self, - session, - hier_attrs, - focus_entity_ids, - parent_id_by_entity_id - ): - all_ids_with_parents = set() - for entity_id in focus_entity_ids: - all_ids_with_parents.add(entity_id) - _entity_id = entity_id - while True: - parent_id = parent_id_by_entity_id.get(_entity_id) - if ( - not parent_id - or parent_id in all_ids_with_parents - ): - break - all_ids_with_parents.add(parent_id) - _entity_id = parent_id - - hier_attr_ids = tuple(hier_attr["id"] for hier_attr in hier_attrs) - hier_attrs_key_by_id = { - hier_attr["id"]: hier_attr["key"] - for hier_attr in hier_attrs - } - - values_per_entity_id = {} - for entity_id in all_ids_with_parents: - values_per_entity_id[entity_id] = {} - for key in hier_attrs_key_by_id.values(): - values_per_entity_id[entity_id][key] = None - - values = query_custom_attributes( - session, hier_attr_ids, all_ids_with_parents, True - ) - for item in values: - entity_id = item["entity_id"] - key = hier_attrs_key_by_id[item["configuration_id"]] - - values_per_entity_id[entity_id][key] = item["value"] - - output = {} - for entity_id in focus_entity_ids: - output[entity_id] = {} - for key in hier_attrs_key_by_id.values(): - value = values_per_entity_id[entity_id][key] - tried_ids = set() - if value is None: - tried_ids.add(entity_id) - _entity_id = entity_id - while value is None: - parent_id = parent_id_by_entity_id.get(_entity_id) - if not parent_id: - break - value = values_per_entity_id[parent_id][key] - if value is not None: - break - _entity_id = parent_id - tried_ids.add(parent_id) - - if value is not None: - for ent_id in tried_ids: - values_per_entity_id[ent_id][key] = value - - output[entity_id][key] = value - return output - def set_task_attr_values( self, session, hier_attrs, task_entity_ids, hier_values_by_entity_id, - parent_id_by_entity_id + parent_id_by_entity_id, + real_values_by_entity_id ): hier_attr_id_by_key = { attr["key"]: attr["id"] for attr in hier_attrs } + filtered_task_ids = set() for task_id in task_entity_ids: - parent_id = parent_id_by_entity_id.get(task_id) or {} + parent_id = parent_id_by_entity_id.get(task_id) parent_values = hier_values_by_entity_id.get(parent_id) - if not parent_values: - continue + if parent_values: + filtered_task_ids.add(task_id) + if not filtered_task_ids: + return + + for task_id in filtered_task_ids: + parent_id = parent_id_by_entity_id[task_id] + parent_values = hier_values_by_entity_id[parent_id] hier_values_by_entity_id[task_id] = {} + real_task_attr_values = real_values_by_entity_id[task_id] for key, value in parent_values.items(): hier_values_by_entity_id[task_id][key] = value + if value is None: + continue + configuration_id = hier_attr_id_by_key[key] _entity_key = collections.OrderedDict([ ("configuration_id", configuration_id), ("entity_id", task_id) ]) - - session.recorded_operations.push( - ftrack_api.operation.UpdateEntityOperation( - "ContextCustomAttributeValue", + op = None + if configuration_id not in real_task_attr_values: + op = ftrack_api.operation.CreateEntityOperation( + "CustomAttributeValue", + _entity_key, + {"value": value} + ) + elif real_task_attr_values[configuration_id] != value: + op = ftrack_api.operation.UpdateEntityOperation( + "CustomAttributeValue", _entity_key, "value", - ftrack_api.symbol.NOT_SET, + real_task_attr_values[configuration_id], value ) - ) - if len(session.recorded_operations) > 100: - session.commit() + + if op is not None: + session.recorded_operations.push(op) + if len(session.recorded_operations) > 100: + session.commit() session.commit() @@ -436,39 +483,68 @@ class PushHierValuesToNonHier(ServerAction): session, entities_by_obj_id, attrs_by_obj_id, - hier_values_by_entity_id + hier_values_by_entity_id, + real_values_by_entity_id ): + """Push values from hierarchical custom attributes to non-hierarchical. + + Args: + session (ftrack_api.Sessison): Session which queried entities, + values and which is used for change propagation. + entities_by_obj_id (dict[str, list[str]]): TypedContext + ftrack entity ids where the attributes are propagated by their + object ids. + attrs_by_obj_id (dict[str, ftrack_api.Entity]): Objects of + 'CustomAttributeConfiguration' by their ids. + hier_values_by_entity_id (doc[str, dict[str, Any]]): Attribute + values by entity id and by their keys. + real_values_by_entity_id (doc[str, dict[str, Any]]): Real attribute + values of entities. + """ + for object_id, entity_ids in entities_by_obj_id.items(): attrs = attrs_by_obj_id.get(object_id) if not attrs or not entity_ids: continue - for attr in attrs: - for entity_id in entity_ids: - value = ( - hier_values_by_entity_id - .get(entity_id, {}) - .get(attr["key"]) - ) + for entity_id in entity_ids: + real_values = real_values_by_entity_id.get(entity_id) + hier_values = hier_values_by_entity_id.get(entity_id) + if hier_values is None: + continue + + for attr in attrs: + attr_id = attr["id"] + attr_key = attr["key"] + value = hier_values.get(attr_key) if value is None: continue _entity_key = collections.OrderedDict([ - ("configuration_id", attr["id"]), + ("configuration_id", attr_id), ("entity_id", entity_id) ]) - session.recorded_operations.push( - ftrack_api.operation.UpdateEntityOperation( - "ContextCustomAttributeValue", + op = None + if attr_id not in real_values: + op = ftrack_api.operation.CreateEntityOperation( + "CustomAttributeValue", + _entity_key, + {"value": value} + ) + elif real_values[attr_id] != value: + op = ftrack_api.operation.UpdateEntityOperation( + "CustomAttributeValue", _entity_key, "value", - ftrack_api.symbol.NOT_SET, + real_values[attr_id], value ) - ) - if len(session.recorded_operations) > 100: - session.commit() + + if op is not None: + session.recorded_operations.push(op) + if len(session.recorded_operations) > 100: + session.commit() session.commit() diff --git a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py index dc76920a57..0f10145c06 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py +++ b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py @@ -1,6 +1,6 @@ import collections -import datetime import copy +from typing import Any import ftrack_api from openpype_modules.ftrack.lib import ( @@ -9,13 +9,30 @@ from openpype_modules.ftrack.lib import ( ) -class PushFrameValuesToTaskEvent(BaseEvent): +class PushHierValuesToNonHierEvent(BaseEvent): + """Push value changes between hierarchical and non-hierarchical attributes. + + Changes of non-hierarchical attributes are pushed to hierarchical and back. + The attributes must have same definition of custom attribute. + + Handler does not handle changes of hierarchical parents. So if entity does + not have explicitly set value of hierarchical attribute and any parent + would change it the change would not be propagated. + + The handler also push the value to task entity on task creation + and movement. To push values between hierarchical & non-hierarchical + add 'Task' to entity types in settings. + + Todos: + Task attribute values push on create/move should be possible to + enabled by settings. + """ + # Ignore event handler by default cust_attrs_query = ( "select id, key, object_type_id, is_hierarchical, default" " from CustomAttributeConfiguration" - " where key in ({}) and" - " (object_type_id in ({}) or is_hierarchical is true)" + " where key in ({})" ) _cached_task_object_id = None @@ -26,35 +43,35 @@ class PushFrameValuesToTaskEvent(BaseEvent): settings_key = "sync_hier_entity_attributes" - def session_user_id(self, session): - if self._cached_user_id is None: - user = session.query( - "User where username is \"{}\"".format(session.api_user) - ).one() - self._cached_user_id = user["id"] - return self._cached_user_id + def filter_entities_info( + self, event: ftrack_api.event.base.Event + ) -> dict[str, list[dict[str, Any]]]: + """Basic entities filter info we care about. - def launch(self, session, event): - filtered_entities_info = self.filter_entities_info(event) - if not filtered_entities_info: - return + This filtering is first of many filters. This does not query anything + from ftrack nor use settings. - for project_id, entities_info in filtered_entities_info.items(): - self.process_by_project(session, event, project_id, entities_info) + Args: + event (ftrack_api.event.base.Event): Ftrack event with update + information. + + Returns: + dict[str, list[dict[str, Any]]]: Filtered entity changes by + project id. + """ - def filter_entities_info(self, event): # Filter if event contain relevant data entities_info = event["data"].get("entities") if not entities_info: return - entities_info_by_project_id = {} + entities_info_by_project_id = collections.defaultdict(list) for entity_info in entities_info: - # Care only about tasks - if entity_info.get("entityType") != "task": + # Ignore removed entities + if entity_info.get("action") == "remove": continue - # Care only about changes of status + # Care only about information with changes of entities changes = entity_info.get("changes") if not changes: continue @@ -69,367 +86,287 @@ class PushFrameValuesToTaskEvent(BaseEvent): if project_id is None: continue - # Skip `Task` entity type if parent didn't change - if entity_info["entity_type"].lower() == "task": - if ( - "parent_id" not in changes - or changes["parent_id"]["new"] is None - ): - continue - - if project_id not in entities_info_by_project_id: - entities_info_by_project_id[project_id] = [] entities_info_by_project_id[project_id].append(entity_info) return entities_info_by_project_id - def process_by_project(self, session, event, project_id, entities_info): - project_name = self.get_project_name_from_event( + def _get_attrs_configurations(self, session, interest_attributes): + """Get custom attribute configurations by name. + + Args: + session (ftrack_api.Session): Ftrack sesson. + interest_attributes (list[str]): Names of custom attributes + that should be synchronized. + + Returns: + tuple[dict[str, list], list]: Attributes by object id and + hierarchical attributes. + """ + + attrs = session.query(self.cust_attrs_query.format( + self.join_query_keys(interest_attributes) + )).all() + + attrs_by_obj_id = collections.defaultdict(list) + hier_attrs = [] + for attr in attrs: + if attr["is_hierarchical"]: + hier_attrs.append(attr) + continue + obj_id = attr["object_type_id"] + attrs_by_obj_id[obj_id].append(attr) + return attrs_by_obj_id, hier_attrs + + def _get_handler_project_settings( + self, + session: ftrack_api.Session, + event: ftrack_api.event.base.Event, + project_id: str + ) -> tuple[set[str], set[str]]: + """Get handler settings based on the project. + + Args: + session (ftrack_api.Session): Ftrack session. + event (ftrack_api.event.base.Event): Ftrack event which triggered + the changes. + project_id (str): Project id where the current changes are handled. + + Returns: + tuple[set[str], set[str]]: Attribute names we care about and + entity types we care about. + """ + + project_name: str = self.get_project_name_from_event( session, event, project_id ) # Load settings - project_settings = self.get_project_settings_from_event( - event, project_name + project_settings: dict[str, Any] = ( + self.get_project_settings_from_event(event, project_name) ) # Load status mapping from presets - event_settings = ( + event_settings: dict[str, Any] = ( project_settings ["ftrack"] ["events"] - ["sync_hier_entity_attributes"] + [self.settings_key] ) # Skip if event is not enabled if not event_settings["enabled"]: self.log.debug("Project \"{}\" has disabled {}".format( project_name, self.__class__.__name__ )) - return + return set(), set() - interest_attributes = event_settings["interest_attributes"] + interest_attributes: list[str] = event_settings["interest_attributes"] if not interest_attributes: self.log.info(( "Project \"{}\" does not have filled 'interest_attributes'," " skipping." )) - return - interest_entity_types = event_settings["interest_entity_types"] + + interest_entity_types: list[str] = ( + event_settings["interest_entity_types"]) if not interest_entity_types: self.log.info(( "Project \"{}\" does not have filled 'interest_entity_types'," " skipping." )) - return - interest_attributes = set(interest_attributes) - interest_entity_types = set(interest_entity_types) + # Unify possible issues from settings ('Asset Build' -> 'assetbuild') + interest_entity_types: set[str] = { + entity_type.replace(" ", "").lower() + for entity_type in interest_entity_types + } + return set(interest_attributes), interest_entity_types - # Separate value changes and task parent changes - _entities_info = [] - added_entities = [] - added_entity_ids = set() - task_parent_changes = [] + def _entities_filter_by_settings( + self, + entities_info: list[dict[str, Any]], + interest_attributes: set[str], + interest_entity_types: set[str] + ): + new_entities_info = [] for entity_info in entities_info: - if entity_info["entity_type"].lower() == "task": - task_parent_changes.append(entity_info) - elif entity_info.get("action") == "add": - added_entities.append(entity_info) - added_entity_ids.add(entity_info["entityId"]) - else: - _entities_info.append(entity_info) - entities_info = _entities_info + entity_type_low = entity_info["entity_type"].lower() - # Filter entities info with changes - interesting_data, changed_keys_by_object_id = self.filter_changes( - session, event, entities_info, interest_attributes - ) - self.interesting_data_for_added( - session, - added_entities, - interest_attributes, - interesting_data, - changed_keys_by_object_id - ) - if not interesting_data and not task_parent_changes: - return + changes = entity_info["changes"] + # SPECIAL CASE: Capture changes of task created/moved under + # interested entity type + if ( + entity_type_low == "task" + and "parent_id" in changes + ): + # Direct parent is always second item in 'parents' and 'Task' + # must have at least one parent + parent_info = entity_info["parents"][1] + parent_entity_type = ( + parent_info["entity_type"] + .replace(" ", "") + .lower() + ) + if parent_entity_type in interest_entity_types: + new_entities_info.append(entity_info) + continue - # Prepare object types - object_types = session.query("select id, name from ObjectType").all() - object_types_by_name = {} - for object_type in object_types: - name_low = object_type["name"].lower() - object_types_by_name[name_low] = object_type + # Skip if entity type is not enabled for attr value sync + if entity_type_low not in interest_entity_types: + continue - # NOTE it would be nice to check if `interesting_data` do not contain - # value changs of tasks that were created or moved - # - it is a complex way how to find out - if interesting_data: - self.process_attribute_changes( - session, - object_types_by_name, - interesting_data, - changed_keys_by_object_id, - interest_entity_types, - interest_attributes, - added_entity_ids - ) + valid_attr_change = entity_info.get("action") == "add" + for attr_key in interest_attributes: + if valid_attr_change: + break - if task_parent_changes: - self.process_task_parent_change( - session, object_types_by_name, task_parent_changes, - interest_entity_types, interest_attributes - ) + if attr_key not in changes: + continue - def process_task_parent_change( + if changes[attr_key]["new"] is not None: + valid_attr_change = True + + if not valid_attr_change: + continue + + new_entities_info.append(entity_info) + + return new_entities_info + + def propagate_attribute_changes( self, session, - object_types_by_name, - task_parent_changes, - interest_entity_types, - interest_attributes + interest_attributes, + entities_info, + attrs_by_obj_id, + hier_attrs, + real_values_by_entity_id, + hier_values_by_entity_id, ): - """Push custom attribute values if task parent has changed. + hier_attr_ids_by_key = { + attr["key"]: attr["id"] + for attr in hier_attrs + } + filtered_interest_attributes = { + attr_name + for attr_name in interest_attributes + if attr_name in hier_attr_ids_by_key + } + attrs_keys_by_obj_id = {} + for obj_id, attrs in attrs_by_obj_id.items(): + attrs_keys_by_obj_id[obj_id] = { + attr["key"]: attr["id"] + for attr in attrs + } - Parent is changed if task is created or if is moved under different - entity. We don't care about all task changes only about those that - have it's parent in interest types (from settings). + op_changes = [] + for entity_info in entities_info: + entity_id = entity_info["entityId"] + obj_id = entity_info["objectTypeId"] + # Skip attributes sync if does not have object specific custom + # attribute + if obj_id not in attrs_keys_by_obj_id: + continue + attr_keys = attrs_keys_by_obj_id[obj_id] + real_values = real_values_by_entity_id[entity_id] + hier_values = hier_values_by_entity_id[entity_id] - Tasks hierarchical value should be unset or set based on parents - real hierarchical value and non hierarchical custom attribute value - should be set to hierarchical value. - """ - - # Store task ids which were created or moved under parent with entity - # type defined in settings (interest_entity_types). - task_ids = set() - # Store parent ids of matching task ids - matching_parent_ids = set() - # Store all entity ids of all entities to be able query hierarchical - # values. - whole_hierarchy_ids = set() - # Store parent id of each entity id - parent_id_by_entity_id = {} - for entity_info in task_parent_changes: - # Ignore entities with less parents than 2 - # NOTE entity itself is also part of "parents" value - parents = entity_info.get("parents") or [] - if len(parents) < 2: + changes = copy.deepcopy(entity_info["changes"]) + obj_id_attr_keys = { + attr_key + for attr_key in filtered_interest_attributes + if attr_key in attr_keys + } + if not obj_id_attr_keys: continue - parent_info = parents[1] - # Check if parent has entity type we care about. - if parent_info["entity_type"] not in interest_entity_types: - continue + value_by_key = {} + is_new_entity = entity_info.get("action") == "add" + for attr_key in obj_id_attr_keys: + if ( + attr_key in changes + and changes[attr_key]["new"] is not None + ): + value_by_key[attr_key] = changes[attr_key]["new"] - task_ids.add(entity_info["entityId"]) - matching_parent_ids.add(parent_info["entityId"]) - - # Store whole hierarchi of task entity - prev_id = None - for item in parents: - item_id = item["entityId"] - whole_hierarchy_ids.add(item_id) - - if prev_id is None: - prev_id = item_id + if not is_new_entity: continue - parent_id_by_entity_id[prev_id] = item_id - if item["entityType"] == "show": - break - prev_id = item_id + hier_attr_id = hier_attr_ids_by_key[attr_key] + attr_id = attr_keys[attr_key] + if hier_attr_id in real_values or attr_id in real_values: + continue - # Just skip if nothing is interesting for our settings - if not matching_parent_ids: - return + value_by_key[attr_key] = hier_values[hier_attr_id] - # Query object type ids of parent ids for custom attribute - # definitions query - entities = session.query( - "select object_type_id from TypedContext where id in ({})".format( - self.join_query_keys(matching_parent_ids) - ) - ) + for key, new_value in value_by_key.items(): + if new_value is None: + continue - # Prepare task object id - task_object_id = object_types_by_name["task"]["id"] + hier_id = hier_attr_ids_by_key[key] + std_id = attr_keys[key] + real_hier_value = real_values.get(hier_id) + real_std_value = real_values.get(std_id) + hier_value = hier_values[hier_id] + # Get right type of value for conversion + # - values in event are strings + type_value = real_hier_value + if type_value is None: + type_value = real_std_value + if type_value is None: + type_value = hier_value + # Skip if current values are not set + if type_value is None: + continue - # All object ids for which we're querying custom attribute definitions - object_type_ids = set() - object_type_ids.add(task_object_id) - for entity in entities: - object_type_ids.add(entity["object_type_id"]) + try: + new_value = type(type_value)(new_value) + except Exception: + self.log.warning(( + "Couldn't convert from {} to {}." + " Skipping update values." + ).format(type(new_value), type(type_value))) + continue - attrs_by_obj_id, hier_attrs = self.attrs_configurations( - session, object_type_ids, interest_attributes - ) + real_std_value_is_same = new_value == real_std_value + real_hier_value_is_same = new_value == real_hier_value + # New value does not match anything in current entity values + if ( + not is_new_entity + and not real_std_value_is_same + and not real_hier_value_is_same + ): + continue - # Skip if all task attributes are not available - task_attrs = attrs_by_obj_id.get(task_object_id) - if not task_attrs: - return + if not real_std_value_is_same: + op_changes.append(( + std_id, + entity_id, + new_value, + real_values.get(std_id), + std_id in real_values + )) - # Skip attributes that is not in both hierarchical and nonhierarchical - # TODO be able to push values if hierarchical is available - for key in interest_attributes: - if key not in hier_attrs: - task_attrs.pop(key, None) + if not real_hier_value_is_same: + op_changes.append(( + hier_id, + entity_id, + new_value, + real_values.get(hier_id), + hier_id in real_values + )) - elif key not in task_attrs: - hier_attrs.pop(key) + for change in op_changes: + ( + attr_id, + entity_id, + new_value, + old_value, + do_update + ) = change - # Skip if nothing remained - if not task_attrs: - return - - # Do some preparations for custom attribute values query - attr_key_by_id = {} - nonhier_id_by_key = {} - hier_attr_ids = [] - for key, attr_id in hier_attrs.items(): - attr_key_by_id[attr_id] = key - hier_attr_ids.append(attr_id) - - conf_ids = list(hier_attr_ids) - task_conf_ids = [] - for key, attr_id in task_attrs.items(): - attr_key_by_id[attr_id] = key - nonhier_id_by_key[key] = attr_id - conf_ids.append(attr_id) - task_conf_ids.append(attr_id) - - # Query custom attribute values - # - result does not contain values for all entities only result of - # query callback to ftrack server - result = query_custom_attributes( - session, list(hier_attr_ids), whole_hierarchy_ids, True - ) - result.extend( - query_custom_attributes( - session, task_conf_ids, whole_hierarchy_ids, False - ) - ) - - # Prepare variables where result will be stored - # - hierachical values should not contain attribute with value by - # default - hier_values_by_entity_id = { - entity_id: {} - for entity_id in whole_hierarchy_ids - } - # - real values of custom attributes - values_by_entity_id = { - entity_id: { - attr_id: None - for attr_id in conf_ids - } - for entity_id in whole_hierarchy_ids - } - for item in result: - attr_id = item["configuration_id"] - entity_id = item["entity_id"] - value = item["value"] - - values_by_entity_id[entity_id][attr_id] = value - - if attr_id in hier_attr_ids and value is not None: - hier_values_by_entity_id[entity_id][attr_id] = value - - # Prepare values for all task entities - # - going through all parents and storing first value value - # - store None to those that are already known that do not have set - # value at all - for task_id in tuple(task_ids): - for attr_id in hier_attr_ids: - entity_ids = [] - value = None - entity_id = task_id - while value is None: - entity_value = hier_values_by_entity_id[entity_id] - if attr_id in entity_value: - value = entity_value[attr_id] - if value is None: - break - - if value is None: - entity_ids.append(entity_id) - - entity_id = parent_id_by_entity_id.get(entity_id) - if entity_id is None: - break - - for entity_id in entity_ids: - hier_values_by_entity_id[entity_id][attr_id] = value - - # Prepare changes to commit - changes = [] - for task_id in tuple(task_ids): - parent_id = parent_id_by_entity_id[task_id] - for attr_id in hier_attr_ids: - attr_key = attr_key_by_id[attr_id] - nonhier_id = nonhier_id_by_key[attr_key] - - # Real value of hierarchical attribute on parent - # - If is none then should be unset - real_parent_value = values_by_entity_id[parent_id][attr_id] - # Current hierarchical value of a task - # - Will be compared to real parent value - hier_value = hier_values_by_entity_id[task_id][attr_id] - - # Parent value that can be inherited from it's parent entity - parent_value = hier_values_by_entity_id[parent_id][attr_id] - # Task value of nonhierarchical custom attribute - nonhier_value = values_by_entity_id[task_id][nonhier_id] - - if real_parent_value != hier_value: - changes.append({ - "new_value": real_parent_value, - "attr_id": attr_id, - "entity_id": task_id, - "attr_key": attr_key - }) - - if parent_value != nonhier_value: - changes.append({ - "new_value": parent_value, - "attr_id": nonhier_id, - "entity_id": task_id, - "attr_key": attr_key - }) - - self._commit_changes(session, changes) - - def _commit_changes(self, session, changes): - uncommited_changes = False - for idx, item in enumerate(changes): - new_value = item["new_value"] - old_value = item["old_value"] - attr_id = item["attr_id"] - entity_id = item["entity_id"] - attr_key = item["attr_key"] - - entity_key = collections.OrderedDict(( + entity_key = collections.OrderedDict([ ("configuration_id", attr_id), ("entity_id", entity_id) - )) - self._cached_changes.append({ - "attr_key": attr_key, - "entity_id": entity_id, - "value": new_value, - "time": datetime.datetime.now() - }) - old_value_is_set = ( - old_value is not ftrack_api.symbol.NOT_SET - and old_value is not None - ) - if new_value is None: - if not old_value_is_set: - continue - op = ftrack_api.operation.DeleteEntityOperation( - "CustomAttributeValue", - entity_key - ) - - elif old_value_is_set: + ]) + if do_update: op = ftrack_api.operation.UpdateEntityOperation( "CustomAttributeValue", entity_key, @@ -446,449 +383,116 @@ class PushFrameValuesToTaskEvent(BaseEvent): ) session.recorded_operations.push(op) - self.log.info(( - "Changing Custom Attribute \"{}\" to value" - " \"{}\" on entity: {}" - ).format(attr_key, new_value, entity_id)) - - if (idx + 1) % 20 == 0: - uncommited_changes = False - try: - session.commit() - except Exception: - session.rollback() - self.log.warning( - "Changing of values failed.", exc_info=True - ) - else: - uncommited_changes = True - if uncommited_changes: - try: + if len(session.recorded_operations) > 100: session.commit() - except Exception: - session.rollback() - self.log.warning("Changing of values failed.", exc_info=True) + session.commit() - def process_attribute_changes( + def process_by_project( self, - session, - object_types_by_name, - interesting_data, - changed_keys_by_object_id, - interest_entity_types, - interest_attributes, - added_entity_ids + session: ftrack_api.Session, + event: ftrack_api.event.base.Event, + project_id: str, + entities_info: list[dict[str, Any]] ): - # Prepare task object id - task_object_id = object_types_by_name["task"]["id"] + """Proces changes in single project. - # Collect object type ids based on settings - interest_object_ids = [] - for entity_type in interest_entity_types: - _entity_type = entity_type.lower() - object_type = object_types_by_name.get(_entity_type) - if not object_type: - self.log.warning("Couldn't find object type \"{}\"".format( - entity_type - )) + Args: + session (ftrack_api.Session): Ftrack session. + event (ftrack_api.event.base.Event): Event which has all changes + information. + project_id (str): Project id related to changes. + entities_info (list[dict[str, Any]]): Changes of entities. + """ - interest_object_ids.append(object_type["id"]) - - # Query entities by filtered data and object ids - entities = self.get_entities( - session, interesting_data, interest_object_ids - ) - if not entities: + ( + interest_attributes, + interest_entity_types + ) = self._get_handler_project_settings(session, event, project_id) + if not interest_attributes or not interest_entity_types: return - # Pop not found entities from interesting data - entity_ids = set( - entity["id"] - for entity in entities + entities_info: list[dict[str, Any]] = ( + self._entities_filter_by_settings( + entities_info, + interest_attributes, + interest_entity_types + ) ) - for entity_id in tuple(interesting_data.keys()): - if entity_id not in entity_ids: - interesting_data.pop(entity_id) - - # Add task object type to list - attr_obj_ids = list(interest_object_ids) - attr_obj_ids.append(task_object_id) - - attrs_by_obj_id, hier_attrs = self.attrs_configurations( - session, attr_obj_ids, interest_attributes - ) - - task_attrs = attrs_by_obj_id.get(task_object_id) - - changed_keys = set() - # Skip keys that are not both in hierachical and type specific - for object_id, keys in changed_keys_by_object_id.items(): - changed_keys |= set(keys) - object_id_attrs = attrs_by_obj_id.get(object_id) - for key in keys: - if key not in hier_attrs: - attrs_by_obj_id[object_id].pop(key) - continue - - if ( - (not object_id_attrs or key not in object_id_attrs) - and (not task_attrs or key not in task_attrs) - ): - hier_attrs.pop(key) - - # Clean up empty values - for key, value in tuple(attrs_by_obj_id.items()): - if not value: - attrs_by_obj_id.pop(key) - - if not attrs_by_obj_id: - self.log.warning(( - "There is not created Custom Attributes {} " - " for entity types: {}" - ).format( - self.join_query_keys(interest_attributes), - self.join_query_keys(interest_entity_types) - )) + if not entities_info: return - # Prepare task entities - task_entities = [] - # If task entity does not contain changed attribute then skip - if task_attrs: - task_entities = self.get_task_entities(session, interesting_data) - - task_entity_ids = set() - parent_id_by_task_id = {} - for task_entity in task_entities: - task_id = task_entity["id"] - task_entity_ids.add(task_id) - parent_id_by_task_id[task_id] = task_entity["parent_id"] - - self.finalize_attribute_changes( - session, - interesting_data, - changed_keys, - attrs_by_obj_id, - hier_attrs, - task_entity_ids, - parent_id_by_task_id, - added_entity_ids - ) - - def finalize_attribute_changes( - self, - session, - interesting_data, - changed_keys, - attrs_by_obj_id, - hier_attrs, - task_entity_ids, - parent_id_by_task_id, - added_entity_ids - ): - attr_id_to_key = {} - for attr_confs in attrs_by_obj_id.values(): - for key in changed_keys: - custom_attr_id = attr_confs.get(key) - if custom_attr_id: - attr_id_to_key[custom_attr_id] = key - - for key in changed_keys: - custom_attr_id = hier_attrs.get(key) - if custom_attr_id: - attr_id_to_key[custom_attr_id] = key - - entity_ids = ( - set(interesting_data.keys()) | task_entity_ids - ) - attr_ids = set(attr_id_to_key.keys()) - - current_values_by_id = self.get_current_values( - session, - attr_ids, - entity_ids, - task_entity_ids, - hier_attrs - ) - - changes = [] - for entity_id, current_values in current_values_by_id.items(): - parent_id = parent_id_by_task_id.get(entity_id) - if not parent_id: - parent_id = entity_id - values = interesting_data[parent_id] - - added_entity = entity_id in added_entity_ids - for attr_id, old_value in current_values.items(): - if added_entity and attr_id in hier_attrs: - continue - - attr_key = attr_id_to_key.get(attr_id) - if not attr_key: - continue - - # Convert new value from string - new_value = values.get(attr_key) - new_value_is_valid = ( - old_value is not ftrack_api.symbol.NOT_SET - and new_value is not None - ) - if added_entity and not new_value_is_valid: - continue - - if new_value is not None and new_value_is_valid: - try: - new_value = type(old_value)(new_value) - except Exception: - self.log.warning(( - "Couldn't convert from {} to {}." - " Skipping update values." - ).format(type(new_value), type(old_value))) - if new_value == old_value: - continue - - changes.append({ - "new_value": new_value, - "attr_id": attr_id, - "old_value": old_value, - "entity_id": entity_id, - "attr_key": attr_key - }) - self._commit_changes(session, changes) - - def filter_changes( - self, session, event, entities_info, interest_attributes - ): - session_user_id = self.session_user_id(session) - user_data = event["data"].get("user") - changed_by_session = False - if user_data and user_data.get("userid") == session_user_id: - changed_by_session = True - - current_time = datetime.datetime.now() - - interesting_data = {} - changed_keys_by_object_id = {} - - for entity_info in entities_info: - # Care only about changes if specific keys - entity_changes = {} - changes = entity_info["changes"] - for key in interest_attributes: - if key in changes: - entity_changes[key] = changes[key]["new"] - - entity_id = entity_info["entityId"] - if changed_by_session: - for key, new_value in tuple(entity_changes.items()): - for cached in tuple(self._cached_changes): - if ( - cached["entity_id"] != entity_id - or cached["attr_key"] != key - ): - continue - - cached_value = cached["value"] - try: - new_value = type(cached_value)(new_value) - except Exception: - pass - - if cached_value == new_value: - self._cached_changes.remove(cached) - entity_changes.pop(key) - break - - delta = (current_time - cached["time"]).seconds - if delta > self._max_delta: - self._cached_changes.remove(cached) - - if not entity_changes: - continue - - entity_id = entity_info["entityId"] - object_id = entity_info["objectTypeId"] - interesting_data[entity_id] = entity_changes - if object_id not in changed_keys_by_object_id: - changed_keys_by_object_id[object_id] = set() - changed_keys_by_object_id[object_id] |= set(entity_changes.keys()) - - return interesting_data, changed_keys_by_object_id - - def interesting_data_for_added( - self, - session, - added_entities, - interest_attributes, - interesting_data, - changed_keys_by_object_id - ): - if not added_entities or not interest_attributes: - return - - object_type_ids = set() - entity_ids = set() - all_entity_ids = set() - object_id_by_entity_id = {} - project_id = None - entity_ids_by_parent_id = collections.defaultdict(set) - for entity_info in added_entities: - object_id = entity_info["objectTypeId"] - entity_id = entity_info["entityId"] - object_type_ids.add(object_id) - entity_ids.add(entity_id) - object_id_by_entity_id[entity_id] = object_id - - for item in entity_info["parents"]: - entity_id = item["entityId"] - all_entity_ids.add(entity_id) - parent_id = item["parentId"] - if not parent_id: - project_id = entity_id - else: - entity_ids_by_parent_id[parent_id].add(entity_id) - - hier_attrs = self.get_hierarchical_configurations( + attrs_by_obj_id, hier_attrs = self._get_attrs_configurations( session, interest_attributes ) - if not hier_attrs: + # Skip if attributes are not available + # - there is nothing to sync + if not attrs_by_obj_id or not hier_attrs: return - hier_attrs_key_by_id = { - attr_conf["id"]: attr_conf["key"] - for attr_conf in hier_attrs - } - default_values_by_key = { - attr_conf["key"]: attr_conf["default"] - for attr_conf in hier_attrs - } + entity_ids_by_parent_id = collections.defaultdict(set) + all_entity_ids = set() + for entity_info in entities_info: + entity_id = None + for item in entity_info["parents"]: + item_id = item["entityId"] + all_entity_ids.add(item_id) + if entity_id is not None: + entity_ids_by_parent_id[item_id].add(entity_id) + entity_id = item_id - values = query_custom_attributes( - session, list(hier_attrs_key_by_id.keys()), all_entity_ids, True + attr_ids = {attr["id"] for attr in hier_attrs} + for attrs in attrs_by_obj_id.values(): + attr_ids |= {attr["id"] for attr in attrs} + + # Query real custom attribute values + # - we have to know what are the real values, if are set and to what + # value + value_items = query_custom_attributes( + session, attr_ids, all_entity_ids, True ) - values_per_entity_id = {} - for entity_id in all_entity_ids: - values_per_entity_id[entity_id] = {} - for attr_name in interest_attributes: - values_per_entity_id[entity_id][attr_name] = None - - for item in values: - entity_id = item["entity_id"] - key = hier_attrs_key_by_id[item["configuration_id"]] - values_per_entity_id[entity_id][key] = item["value"] - - fill_queue = collections.deque() - fill_queue.append((project_id, default_values_by_key)) - while fill_queue: - item = fill_queue.popleft() - entity_id, values_by_key = item - entity_values = values_per_entity_id[entity_id] - new_values_by_key = copy.deepcopy(values_by_key) - for key, value in values_by_key.items(): - current_value = entity_values[key] - if current_value is None: - entity_values[key] = value - else: - new_values_by_key[key] = current_value - - for child_id in entity_ids_by_parent_id[entity_id]: - fill_queue.append((child_id, new_values_by_key)) - - for entity_id in entity_ids: - entity_changes = {} - for key, value in values_per_entity_id[entity_id].items(): - if value is not None: - entity_changes[key] = value - - if not entity_changes: - continue - - interesting_data[entity_id] = entity_changes - object_id = object_id_by_entity_id[entity_id] - if object_id not in changed_keys_by_object_id: - changed_keys_by_object_id[object_id] = set() - changed_keys_by_object_id[object_id] |= set(entity_changes.keys()) - - def get_current_values( - self, - session, - attr_ids, - entity_ids, - task_entity_ids, - hier_attrs - ): - current_values_by_id = {} - if not attr_ids or not entity_ids: - return current_values_by_id - - for entity_id in entity_ids: - current_values_by_id[entity_id] = {} - for attr_id in attr_ids: - current_values_by_id[entity_id][attr_id] = ( - ftrack_api.symbol.NOT_SET - ) - - values = query_custom_attributes( - session, attr_ids, entity_ids, True - ) - - for item in values: + real_values_by_entity_id = collections.defaultdict(dict) + for item in value_items: entity_id = item["entity_id"] attr_id = item["configuration_id"] - if entity_id in task_entity_ids and attr_id in hier_attrs: - continue + real_values_by_entity_id[entity_id][attr_id] = item["value"] - if entity_id not in current_values_by_id: - current_values_by_id[entity_id] = {} - current_values_by_id[entity_id][attr_id] = item["value"] - return current_values_by_id + hier_values_by_entity_id = {} + default_values = { + attr["id"]: attr["default"] + for attr in hier_attrs + } + hier_queue = collections.deque() + hier_queue.append((default_values, [project_id])) + while hier_queue: + parent_values, entity_ids = hier_queue.popleft() + for entity_id in entity_ids: + entity_values = copy.deepcopy(parent_values) + real_values = real_values_by_entity_id[entity_id] + for attr_id, value in real_values.items(): + entity_values[attr_id] = value + hier_values_by_entity_id[entity_id] = entity_values + hier_queue.append( + (entity_values, entity_ids_by_parent_id[entity_id]) + ) - def get_entities(self, session, interesting_data, interest_object_ids): - return session.query(( - "select id from TypedContext" - " where id in ({}) and object_type_id in ({})" - ).format( - self.join_query_keys(interesting_data.keys()), - self.join_query_keys(interest_object_ids) - )).all() - - def get_task_entities(self, session, interesting_data): - return session.query( - "select id, parent_id from Task where parent_id in ({})".format( - self.join_query_keys(interesting_data.keys()) - ) - ).all() - - def attrs_configurations(self, session, object_ids, interest_attributes): - attrs = session.query(self.cust_attrs_query.format( - self.join_query_keys(interest_attributes), - self.join_query_keys(object_ids) - )).all() - - output = {} - hiearchical = {} - for attr in attrs: - if attr["is_hierarchical"]: - hiearchical[attr["key"]] = attr["id"] - continue - obj_id = attr["object_type_id"] - if obj_id not in output: - output[obj_id] = {} - output[obj_id][attr["key"]] = attr["id"] - return output, hiearchical - - def get_hierarchical_configurations(self, session, interest_attributes): - hier_attr_query = ( - "select id, key, object_type_id, is_hierarchical, default" - " from CustomAttributeConfiguration" - " where key in ({}) and is_hierarchical is true" + self.propagate_attribute_changes( + session, + interest_attributes, + entities_info, + attrs_by_obj_id, + hier_attrs, + real_values_by_entity_id, + hier_values_by_entity_id, ) - if not interest_attributes: - return [] - return list(session.query(hier_attr_query.format( - self.join_query_keys(interest_attributes), - )).all()) + + def launch(self, session, event): + filtered_entities_info = self.filter_entities_info(event) + if not filtered_entities_info: + return + + for project_id, entities_info in filtered_entities_info.items(): + self.process_by_project(session, event, project_id, entities_info) def register(session): - PushFrameValuesToTaskEvent(session).register() + PushHierValuesToNonHierEvent(session).register() From 0cc5f3ee37e08aa5516d0ca11bebf1c7fea6fd0e Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Fri, 24 Mar 2023 15:44:29 +0100 Subject: [PATCH 138/228] Changed logic as suggested --- .../modules/kitsu/plugins/publish/integrate_kitsu_note.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py index 6a1b09dde1..571ed683bb 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py @@ -82,8 +82,6 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): if instance.data.get("publish") } - allow_status_change = False - # Check if any family requirement is met for family_requirement in self.status_change_conditions[ "family_requirements" @@ -92,8 +90,8 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): for family in families: match = family_requirement["family"].lower() == family - if match and condition or not condition and not match: - allow_status_change = True + if match and not condition or condition and not match: + allow_status_change = False break if allow_status_change: From aceaef0b213833214472643f580a01121778f143 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 24 Mar 2023 16:24:33 +0100 Subject: [PATCH 139/228] hiero: correct container colors if UpToDate --- openpype/hosts/hiero/api/lib.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py index bbd1edc14a..0d4368529f 100644 --- a/openpype/hosts/hiero/api/lib.py +++ b/openpype/hosts/hiero/api/lib.py @@ -1221,7 +1221,7 @@ def set_track_color(track_item, color): def check_inventory_versions(track_items=None): """ - Actual version color idetifier of Loaded containers + Actual version color identifier of Loaded containers Check all track items and filter only Loader nodes for its version. It will get all versions from database @@ -1249,10 +1249,10 @@ def check_inventory_versions(track_items=None): project_name = legacy_io.active_project() filter_result = filter_containers(containers, project_name) for container in filter_result.latest: - set_track_color(container["_item"], clip_color) + set_track_color(container["_item"], clip_color_last) for container in filter_result.outdated: - set_track_color(container["_item"], clip_color_last) + set_track_color(container["_item"], clip_color) def selection_changed_timeline(event): From 1531708236cf9f27e459c2b20cd642e5f08905af Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 24 Mar 2023 16:30:46 +0100 Subject: [PATCH 140/228] Global: persistent staging directory for renders (#4583) * OP-4258 - Settings for transient template * OP-4258 - added collector for transient staging dir Allows setting profiles to create persistent stagingDir. * OP-4258 - implemented persistent stagingDir in cleanup * OP-4258 - updated logging * OP-4258 - updated settings * OP-4258 - Hound * OP-4258 - renamed class to better name * OP-4258 - changed location of Settings Should be used in create and collecting phase also. * OP-4258 - remove version placeholder from transient template It was discussed that it shouldn't be used for now. * OP-4258 - extracted transient dir query logic This should be used in collection and creation phase for DCCs which are storing staging dir path directly into nodes. * OP-4258 - added use of scene_name placeholder in collector DCC dependent, way how to implement versioning, might not be used. * OP-4258 - fix scene_name * OP-4258 - remove wrong defaults * OP-4258 - added possibility of different template name Studio might want to put renders to different place from caches. * OP-4258 - renamed according to GH comments * OP-4258 - use is active filter * OP-4258 - use is active filter * OP-4793 - added project_settings to signature * OP-4793 - updated logging message * OP-4793 - added documentation * OP-4258 - fix function arguments * OP-4258 - updates to documentation * OP-4258 - added known issues to documentation --------- Co-authored-by: Roy Nieterau --- openpype/pipeline/publish/contants.py | 1 + openpype/pipeline/publish/lib.py | 80 +++++++++++++++++- openpype/plugins/publish/cleanup.py | 4 + openpype/plugins/publish/cleanup_farm.py | 2 +- .../publish/collect_custom_staging_dir.py | 67 +++++++++++++++ .../defaults/project_anatomy/templates.json | 6 +- .../defaults/project_settings/global.json | 3 +- .../schemas/schema_global_tools.json | 65 ++++++++++++++ .../global_tools_custom_staging_dir.png | Bin 0 -> 9940 bytes .../settings_project_global.md | 29 ++++++- 10 files changed, 249 insertions(+), 8 deletions(-) create mode 100644 openpype/plugins/publish/collect_custom_staging_dir.py create mode 100644 website/docs/project_settings/assets/global_tools_custom_staging_dir.png diff --git a/openpype/pipeline/publish/contants.py b/openpype/pipeline/publish/contants.py index 169eca2e5c..c5296afe9a 100644 --- a/openpype/pipeline/publish/contants.py +++ b/openpype/pipeline/publish/contants.py @@ -1,2 +1,3 @@ DEFAULT_PUBLISH_TEMPLATE = "publish" DEFAULT_HERO_PUBLISH_TEMPLATE = "hero" +TRANSIENT_DIR_TEMPLATE = "transient" diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index 1ec641bac4..81913bcdd5 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -20,13 +20,15 @@ from openpype.settings import ( get_system_settings, ) from openpype.pipeline import ( - tempdir + tempdir, + Anatomy ) from openpype.pipeline.plugin_discover import DiscoverResult from .contants import ( DEFAULT_PUBLISH_TEMPLATE, DEFAULT_HERO_PUBLISH_TEMPLATE, + TRANSIENT_DIR_TEMPLATE ) @@ -690,3 +692,79 @@ def get_publish_repre_path(instance, repre, only_published=False): if os.path.exists(src_path): return src_path return None + + +def get_custom_staging_dir_info(project_name, host_name, family, task_name, + task_type, subset_name, + project_settings=None, + anatomy=None, log=None): + """Checks profiles if context should use special custom dir as staging. + + Args: + project_name (str) + host_name (str) + family (str) + task_name (str) + task_type (str) + subset_name (str) + project_settings(Dict[str, Any]): Prepared project settings. + anatomy (Dict[str, Any]) + log (Logger) (optional) + + Returns: + (tuple) + Raises: + ValueError - if misconfigured template should be used + """ + settings = project_settings or get_project_settings(project_name) + custom_staging_dir_profiles = (settings["global"] + ["tools"] + ["publish"] + ["custom_staging_dir_profiles"]) + if not custom_staging_dir_profiles: + return None, None + + if not log: + log = Logger.get_logger("get_custom_staging_dir_info") + + filtering_criteria = { + "hosts": host_name, + "families": family, + "task_names": task_name, + "task_types": task_type, + "subsets": subset_name + } + profile = filter_profiles(custom_staging_dir_profiles, + filtering_criteria, + logger=log) + + if not profile or not profile["active"]: + return None, None + + if not anatomy: + anatomy = Anatomy(project_name) + + template_name = profile["template_name"] or TRANSIENT_DIR_TEMPLATE + _validate_transient_template(project_name, template_name, anatomy) + + custom_staging_dir = anatomy.templates[template_name]["folder"] + is_persistent = profile["custom_staging_dir_persistent"] + + return custom_staging_dir, is_persistent + + +def _validate_transient_template(project_name, template_name, anatomy): + """Check that transient template is correctly configured. + + Raises: + ValueError - if misconfigured template + """ + if template_name not in anatomy.templates: + raise ValueError(("Anatomy of project \"{}\" does not have set" + " \"{}\" template key!" + ).format(project_name, template_name)) + + if "folder" not in anatomy.templates[template_name]: + raise ValueError(("There is not set \"folder\" template in \"{}\" anatomy" # noqa + " for project \"{}\"." + ).format(template_name, project_name)) diff --git a/openpype/plugins/publish/cleanup.py b/openpype/plugins/publish/cleanup.py index ef312e391f..b90c88890d 100644 --- a/openpype/plugins/publish/cleanup.py +++ b/openpype/plugins/publish/cleanup.py @@ -93,6 +93,10 @@ class CleanUp(pyblish.api.InstancePlugin): self.log.info("No staging directory found: %s" % staging_dir) return + if instance.data.get("stagingDir_persistent"): + self.log.info("Staging dir: %s should be persistent" % staging_dir) + return + self.log.info("Removing staging directory {}".format(staging_dir)) shutil.rmtree(staging_dir) diff --git a/openpype/plugins/publish/cleanup_farm.py b/openpype/plugins/publish/cleanup_farm.py index b87d4698a2..8052f13734 100644 --- a/openpype/plugins/publish/cleanup_farm.py +++ b/openpype/plugins/publish/cleanup_farm.py @@ -37,7 +37,7 @@ class CleanUpFarm(pyblish.api.ContextPlugin): dirpaths_to_remove = set() for instance in context: staging_dir = instance.data.get("stagingDir") - if staging_dir: + if staging_dir and not instance.data.get("stagingDir_persistent"): dirpaths_to_remove.add(os.path.normpath(staging_dir)) if "representations" in instance.data: diff --git a/openpype/plugins/publish/collect_custom_staging_dir.py b/openpype/plugins/publish/collect_custom_staging_dir.py new file mode 100644 index 0000000000..72ab0fe34d --- /dev/null +++ b/openpype/plugins/publish/collect_custom_staging_dir.py @@ -0,0 +1,67 @@ +""" +Requires: + anatomy + + +Provides: + instance.data -> stagingDir (folder path) + -> stagingDir_persistent (bool) +""" +import copy +import os.path + +import pyblish.api + +from openpype.pipeline.publish.lib import get_custom_staging_dir_info + + +class CollectCustomStagingDir(pyblish.api.InstancePlugin): + """Looks through profiles if stagingDir should be persistent and in special + location. + + Transient staging dir could be useful in specific use cases where is + desirable to have temporary renders in specific, persistent folders, could + be on disks optimized for speed for example. + + It is studio responsibility to clean up obsolete folders with data. + + Location of the folder is configured in `project_anatomy/templates/others`. + ('transient' key is expected, with 'folder' key) + + Which family/task type/subset is applicable is configured in: + `project_settings/global/tools/publish/custom_staging_dir_profiles` + + """ + label = "Collect Custom Staging Directory" + order = pyblish.api.CollectorOrder + 0.4990 + + template_key = "transient" + + def process(self, instance): + family = instance.data["family"] + subset_name = instance.data["subset"] + host_name = instance.context.data["hostName"] + project_name = instance.context.data["projectName"] + + anatomy = instance.context.data["anatomy"] + anatomy_data = copy.deepcopy(instance.data["anatomyData"]) + task = anatomy_data.get("task", {}) + + transient_tml, is_persistent = get_custom_staging_dir_info( + project_name, host_name, family, task.get("name"), + task.get("type"), subset_name, anatomy=anatomy, log=self.log) + result_str = "Not adding" + if transient_tml: + anatomy_data["root"] = anatomy.roots + scene_name = instance.context.data.get("currentFile") + if scene_name: + anatomy_data["scene_name"] = os.path.basename(scene_name) + transient_dir = transient_tml.format(**anatomy_data) + instance.data["stagingDir"] = transient_dir + + instance.data["stagingDir_persistent"] = is_persistent + result_str = "Adding '{}' as".format(transient_dir) + + self.log.info("{} custom staging dir for instance with '{}'".format( + result_str, family + )) diff --git a/openpype/settings/defaults/project_anatomy/templates.json b/openpype/settings/defaults/project_anatomy/templates.json index 02c0e35377..e5e535bf19 100644 --- a/openpype/settings/defaults/project_anatomy/templates.json +++ b/openpype/settings/defaults/project_anatomy/templates.json @@ -58,12 +58,16 @@ "file": "{originalBasename}.{ext}", "path": "{@folder}/{@file}" }, + "transient": { + "folder": "{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{family}/{subset}" + }, "__dynamic_keys_labels__": { "maya2unreal": "Maya to Unreal", "simpleUnrealTextureHero": "Simple Unreal Texture - Hero", "simpleUnrealTexture": "Simple Unreal Texture", "online": "online", - "source": "source" + "source": "source", + "transient": "transient" } } } diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index aad17d54da..aba840da78 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -591,7 +591,8 @@ "task_names": [], "template_name": "simpleUnrealTextureHero" } - ] + ], + "custom_staging_dir_profiles": [] } }, "project_folder_structure": "{\"__project_root__\": {\"prod\": {}, \"resources\": {\"footage\": {\"plates\": {}, \"offline\": {}}, \"audio\": {}, \"art_dept\": {}}, \"editorial\": {}, \"assets\": {\"characters\": {}, \"locations\": {}}, \"shots\": {}}}", diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json index 962008d476..85ec482e73 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json @@ -408,6 +408,71 @@ } ] } + }, + { + "type": "list", + "key": "custom_staging_dir_profiles", + "label": "Custom Staging Dir Profiles", + "use_label_wrap": true, + "docstring": "Profiles to specify special location and persistence for staging dir. Could be used in Creators and Publish phase!", + "object_type": { + "type": "dict", + "children": [ + { + "type": "boolean", + "key": "active", + "label": "Is active", + "default": true + }, + { + "type": "separator" + }, + { + "key": "hosts", + "label": "Host names", + "type": "hosts-enum", + "multiselection": true + }, + { + "key": "task_types", + "label": "Task types", + "type": "task-types-enum" + }, + { + "key": "task_names", + "label": "Task names", + "type": "list", + "object_type": "text" + }, + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "text" + }, + { + "key": "subsets", + "label": "Subset names", + "type": "list", + "object_type": "text" + }, + { + "type": "separator" + }, + { + "key": "custom_staging_dir_persistent", + "label": "Custom Staging Folder Persistent", + "type": "boolean", + "default": false + }, + { + "key": "template_name", + "label": "Template Name", + "type": "text", + "placeholder": "transient" + } + ] + } } ] } diff --git a/website/docs/project_settings/assets/global_tools_custom_staging_dir.png b/website/docs/project_settings/assets/global_tools_custom_staging_dir.png new file mode 100644 index 0000000000000000000000000000000000000000..f7e8660a8863239310a1934a49c2811b4e0c031b GIT binary patch literal 9940 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 VmwKMN3A}U$l9y4I&XY9q`!5KAZJhuB literal 0 HcmV?d00001 diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index b6d624caa8..821585ae21 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -230,10 +230,10 @@ Applicable context filters: ## Tools Settings for OpenPype tools. -## Creator +### Creator Settings related to [Creator tool](artist_tools_creator). -### Subset name profiles +#### Subset name profiles ![global_tools_creator_subset_template](assets/global_tools_creator_subset_template.png) Subset name helps to identify published content. More specific name helps with organization and avoid mixing of published content. Subset name is defined using one of templates defined in **Subset name profiles settings**. The template is filled with context information at the time of creation. @@ -263,10 +263,31 @@ Template may look like `"{family}{Task}{Variant}"`. Some creators may have other keys as their context may require more information or more specific values. Make sure you've read documentation of host you're using. -## Workfiles +### Publish + +#### Custom Staging Directory Profiles +With this feature, users can specify a custom data folder path based on presets, which can be used during the creation and publishing stages. + +![global_tools_custom_staging_dir](assets/global_tools_custom_staging_dir.png) + +Staging directories are used as a destination for intermediate files (as renders) before they are renamed and copied to proper location during the integration phase. They could be created completely dynamically in the temp folder or for some DCCs in the `work` area. +Example could be Nuke where artist might want to temporarily render pictures into `work` area to check them before they get published with the choice of "Use existing frames" on the write node. + +One of the key advantages of this feature is that it allows users to choose the folder for writing such intermediate files to take advantage of faster storage for rendering, which can help improve workflow efficiency. Additionally, this feature allows users to keep their intermediate extracted data persistent, and use their own infrastructure for regular cleaning. + +In some cases, these DCCs (Nuke, Houdini, Maya) automatically add a rendering path during the creation stage, which is then used in publishing. Creators and extractors of such DCCs need to use these profiles to fill paths in DCC's nodes to use this functionality. + +The custom staging folder uses a path template configured in `project_anatomy/templates/others` with `transient` being a default example path that could be used. The template requires a 'folder' key for it to be usable as custom staging folder. + +##### Known issues +- Any DCC that uses prefilled paths and store them inside of workfile nodes needs to implement resolving these paths with a configured profiles. +- If studio uses Site Sync remote artists need to have access to configured custom staging folder! +- Each node on the rendering farm must have access to configured custom staging folder! + +### Workfiles All settings related to Workfile tool. -### Open last workfile at launch +#### Open last workfile at launch This feature allows you to define a rule for each task/host or toggle the feature globally to all tasks as they are visible in the picture. ![global_tools_workfile_open_last_version](assets/global_tools_workfile_open_last_version.png) From 09faa840aeff7b479c865fe04185689b1aa75f5f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 24 Mar 2023 17:18:33 +0100 Subject: [PATCH 141/228] updating project_action --- .github/workflows/project_actions.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/project_actions.yml b/.github/workflows/project_actions.yml index 9c8c9da7dd..ec5578d2fe 100644 --- a/.github/workflows/project_actions.yml +++ b/.github/workflows/project_actions.yml @@ -2,9 +2,7 @@ name: project-actions on: pull_request: - types: [review_requested, closed] - pull_request_review: - types: [submitted] + types: [review_requested] jobs: pr_review_requested: @@ -16,7 +14,6 @@ jobs: uses: leonsteinhaeuser/project-beta-automations@v2.1.0 with: gh_token: ${{ secrets.YNPUT_BOT_TOKEN }} - user: ${{ secrets.CI_USER }} organization: ynput project_id: 11 resource_node_id: ${{ github.event.pull_request.node_id }} From 3253c03e3b147c69609e1faf0730f955e32add86 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 24 Mar 2023 17:29:34 +0100 Subject: [PATCH 142/228] returning trigger to project action --- .github/workflows/project_actions.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/project_actions.yml b/.github/workflows/project_actions.yml index ec5578d2fe..ca94f3ae77 100644 --- a/.github/workflows/project_actions.yml +++ b/.github/workflows/project_actions.yml @@ -3,6 +3,8 @@ name: project-actions on: pull_request: types: [review_requested] + pull_request_review: + types: [submitted] jobs: pr_review_requested: From 2baabed6be7eb6977cb7fcb0d69b7c79e1b18327 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 24 Mar 2023 18:50:09 +0100 Subject: [PATCH 143/228] Work in progress extract look cleanup - Fix file hashing so it includes arguments to maketx - Fix maketx destination colorspace when OCIO is enabled - Use pre-collected colorspaces of the resources instead of trying to retrieve again - Fix colorspace attributes being reinterpreted by maya on export (fix remapping) - Fix support for checking config path of maya default OCIO config (due to using `lib.get_color_management_preferences` which remaps that path) --- .../maya/plugins/publish/extract_look.py | 413 ++++++++++-------- 1 file changed, 220 insertions(+), 193 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 447c9a615c..5c03aa5a5a 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -149,22 +149,6 @@ class ExtractLook(publish.Extractor): scene_type = "ma" look_data_type = "json" - @staticmethod - def get_renderer_name(): - """Get renderer name from Maya. - - Returns: - str: Renderer name. - - """ - renderer = cmds.getAttr( - "defaultRenderGlobals.currentRenderer" - ).lower() - # handle various renderman names - if renderer.startswith("renderman"): - renderer = "renderman" - return renderer - def get_maya_scene_type(self, instance): """Get Maya scene type from settings. @@ -209,11 +193,9 @@ class ExtractLook(publish.Extractor): maya_path = os.path.join(dir_path, maya_fname) json_path = os.path.join(dir_path, json_fname) - self.log.info("Performing extraction..") - # Remove all members of the sets so they are not included in the # exported file by accident - self.log.info("Extract sets (%s) ..." % _scene_type) + self.log.info("Processing sets..") lookdata = instance.data["lookData"] relationships = lookdata["relationships"] sets = list(relationships.keys()) @@ -221,6 +203,7 @@ class ExtractLook(publish.Extractor): self.log.info("No sets found") return + self.log.debug("Processing resources..") results = self.process_resources(instance, staging_dir=dir_path) transfers = results["fileTransfers"] hardlinks = results["fileHardlinks"] @@ -228,6 +211,7 @@ class ExtractLook(publish.Extractor): remap = results["attrRemap"] # Extract in correct render layer + self.log.info("Extracting look maya scene file: {}".format(maya_path)) layer = instance.data.get("renderlayer", "defaultRenderLayer") with lib.renderlayer(layer): # TODO: Ensure membership edits don't become renderlayer overrides @@ -299,40 +283,39 @@ class ExtractLook(publish.Extractor): # Source hash for the textures instance.data["sourceHashes"] = hashes - """ - self.log.info("Returning colorspaces to their original values ...") - for attr, value in remap.items(): - self.log.info(" - {}: {}".format(attr, value)) - cmds.setAttr(attr, value, type="string") - """ self.log.info("Extracted instance '%s' to: %s" % (instance.name, maya_path)) - def process_resources(self, instance, staging_dir): + def _set_resource_result_colorspace(self, resource, colorspace): + """Update resource resulting colorspace after texture processing""" + if "result_colorspace" in resource: + if resource["result_colorspace"] == colorspace: + return + + self.log.warning( + "Resource already has a resulting colorspace but is now " + "being overridden to a new one: {} -> {}".format( + resource["result_colorspace"], colorspace + ) + ) + resource["result_colorspace"] = colorspace + + def process_resources(self, instance, staging_dir): + """Process all resources in the instance. + + It is assumed that all resources are nodes using file textures. + + Extract the textures to transfer, possibly convert with maketx and + remap the node paths to the destination path. Note that a source + might be included more than once amongst the resources as they could + be the input file to multiple nodes. + + """ - # Extract the textures to transfer, possibly convert with maketx and - # remap the node paths to the destination path. Note that a source - # might be included more than once amongst the resources as they could - # be the input file to multiple nodes. resources = instance.data["resources"] do_maketx = instance.data.get("maketx", False) + color_management = lib.get_color_management_preferences() - # Collect all unique files used in the resources - files_metadata = {} - for resource in resources: - # Preserve color space values (force value after filepath change) - # This will also trigger in the same order at end of context to - # ensure after context it's still the original value. - color_space = resource.get("color_space") - - for f in resource["files"]: - files_metadata[os.path.normpath(f)] = { - "color_space": color_space} - - # Process the resource files - transfers = [] - hardlinks = [] - hashes = {} # Temporary fix to NOT create hardlinks on windows machines if platform.system().lower() == "windows": self.log.info( @@ -342,58 +325,83 @@ class ExtractLook(publish.Extractor): else: force_copy = instance.data.get("forceCopy", False) - for filepath in files_metadata: - - linearize = False - # if OCIO color management enabled - # it won't take the condition of the files_metadata - - ocio_maya = cmds.colorManagementPrefs(q=True, - cmConfigFileEnabled=True, - cmEnabled=True) - - if do_maketx and not ocio_maya: - if files_metadata[filepath]["color_space"].lower() == "srgb": # noqa: E501 - linearize = True - # set its file node to 'raw' as tx will be linearized - files_metadata[filepath]["color_space"] = "Raw" - - # if do_maketx: - # color_space = "Raw" - - source, mode, texture_hash = self._process_texture( - filepath, - resource, - do_maketx, - staging=staging_dir, - linearize=linearize, - force=force_copy - ) - destination = self.resource_destination(instance, - source, - do_maketx) - - # Force copy is specified. - if force_copy: - mode = COPY - - if mode == COPY: - transfers.append((source, destination)) - self.log.info('file will be copied {} -> {}'.format( - source, destination)) - elif mode == HARDLINK: - hardlinks.append((source, destination)) - self.log.info('file will be hardlinked {} -> {}'.format( - source, destination)) - - # Store the hashes from hash to destination to include in the - # database - hashes[texture_hash] = destination - - # Remap the resources to the destination path (change node attributes) + # Process all resource's individual files + processed_files = {} + transfers = [] + hardlinks = [] + hashes = {} destinations = {} - remap = OrderedDict() # needs to be ordered, see color space values + remap = OrderedDict() for resource in resources: + colorspace = resource["color_space"] + + for filepath in resource["files"]: + filepath = os.path.normpath(filepath) + + if filepath in processed_files: + # The file was already processed, likely due to usage by + # another resource in the scene. We confirm here it + # didn't do color spaces different than the current + # resource. + processed_file = processed_files[filepath] + processed_colorspace = processed_file["color_space"] + processed_result_colorspace = processed_file["result_color_space"] + self.log.debug( + "File was already processed. Likely used by another " + "resource too: {}".format(filepath) + ) + + if colorspace != processed_file["color_space"]: + self.log.warning( + "File was already processed but using another" + "colorspace: {} <-> {}" + "".format(colorspace, processed_colorspace)) + + self._set_resource_result_colorspace( + resource, colorspace=processed_result_colorspace + ) + continue + + texture_result = self._process_texture( + filepath, + do_maketx=do_maketx, + staging_dir=staging_dir, + force_copy=force_copy, + color_management=color_management, + colorspace=colorspace + ) + source, mode, texture_hash, result_colorspace = texture_result + destination = self.resource_destination(instance, + source, + do_maketx) + + # Set the resulting color space on the resource + self._set_resource_result_colorspace( + resource, colorspace=result_colorspace + ) + + processed_files[filepath] = { + "color_space": colorspace, + "result_color_space": result_colorspace, + } + + # Force copy is specified. + if force_copy: + mode = COPY + + if mode == COPY: + transfers.append((source, destination)) + self.log.info('file will be copied {} -> {}'.format( + source, destination)) + elif mode == HARDLINK: + hardlinks.append((source, destination)) + self.log.info('file will be hardlinked {} -> {}'.format( + source, destination)) + + # Store the hashes from hash to destination to include in the + # database + hashes[texture_hash] = destination + source = os.path.normpath(resource["source"]) if source not in destinations: # Cache destination as source resource might be included @@ -402,35 +410,23 @@ class ExtractLook(publish.Extractor): instance, source, do_maketx ) + # Set up remapping attributes for the node during the publish + # The order of these can be important if one attribute directly + # affects another, e.g. we set colorspace after filepath because + # maya sometimes tries to guess the colorspace when changing + # filepaths (which is avoidable, but we don't want to have those + # attributes changed in the resulting publish) + # Remap filepath to publish destination + filepath_attr = resource["attribute"] + remap[filepath_attr] = destinations[source] + # Preserve color space values (force value after filepath change) # This will also trigger in the same order at end of context to # ensure after context it's still the original value. - color_space_attr = resource["node"] + ".colorSpace" - try: - color_space = cmds.getAttr(color_space_attr) - except ValueError: - # node doesn't have color space attribute - color_space = "Raw" - else: - # get the resolved files - metadata = files_metadata.get(source) - # if the files are unresolved from `source` - # assume color space from the first file of - # the resource - if not metadata: - first_file = next(iter(resource.get( - "files", [])), None) - if not first_file: - continue - first_filepath = os.path.normpath(first_file) - metadata = files_metadata[first_filepath] - if metadata["color_space"] == "Raw": - # set color space to raw if we linearized it - color_space = "Raw" - # Remap file node filename to destination - remap[color_space_attr] = color_space - attr = resource["attribute"] - remap[attr] = destinations[source] + node = resource["node"] + if cmds.attributeQuery("colorSpace", node=node, exists=True): + color_space_attr = "{}.colorSpace".format(node) + remap[color_space_attr] = resource["result_color_space"] self.log.info("Finished remapping destinations ...") @@ -469,91 +465,115 @@ class ExtractLook(publish.Extractor): resources_dir, basename + ext ) - def _process_texture(self, filepath, resource, - do_maketx, staging, linearize, force): - """Process a single texture file on disk for publishing. - This will: - 1. Check whether it's already published, if so it will do hardlink - 2. If not published and maketx is enabled, generate a new .tx file. - 3. Compute the destination path for the source file. - Args: - filepath (str): The source file path to process. - do_maketx (bool): Whether to produce a .tx file - Returns: - """ - - fname, ext = os.path.splitext(os.path.basename(filepath)) - - args = [] - if do_maketx: - args.append("maketx") - texture_hash = source_hash(filepath, *args) + def _get_existing_hashed_texture(self, texture_hash): + """Return the first found filepath from a texture hash""" # If source has been published before with the same settings, # then don't reprocess but hardlink from the original existing = find_paths_by_hash(texture_hash) - if existing and not force: + if existing: self.log.info("Found hash in database, preparing hardlink..") source = next((p for p in existing if os.path.exists(p)), None) if source: return source, HARDLINK, texture_hash else: self.log.warning( - ("Paths not found on disk, " - "skipping hardlink: %s") % (existing,) + "Paths not found on disk, " + "skipping hardlink: {}".format(existing) ) + def _process_texture(self, + filepath, + do_maketx, + staging_dir, + force_copy, + color_management, + colorspace): + """Process a single texture file on disk for publishing. + This will: + 1. Check whether it's already published, if so it will do hardlink + 2. If not published and maketx is enabled, generate a new .tx file. + 3. Compute the destination path for the source file. + + Args: + filepath (str): The source file path to process. + do_maketx (bool): Whether to produce a .tx file + staging_dir (str): The staging directory to write to. + force_copy (bool): Whether to force a copy even if a file hash + might have existed already in the project, otherwise + hardlinking the existing file is allowed. + color_management (dict): Maya's Color Management settings from + `lib.get_color_management_preferences` + colorspace (str): The source colorspace of the resources this + texture belongs to. + + Returns: + tuple: (filepath, copy_mode, texture_hash, result_colorspace) + """ + + fname, ext = os.path.splitext(os.path.basename(filepath)) + + # Note: The texture hash is only reliable if we include any potential + # conversion arguments provide to e.g. `maketx` + args = [] + hash_args = [] + if do_maketx and ext != ".tx": - # Produce .tx file in staging if source file is not .tx - converted = os.path.join(staging, "resources", fname + ".tx") - additional_args = [ + # Define .tx filepath in staging if source file is not .tx + converted = os.path.join(staging_dir, "resources", fname + ".tx") + + if color_management["enabled"]: + config_path = color_management["config"] + if not os.path.exists(config_path): + raise RuntimeError("OCIO config not found at: " + "{}".format(config_path)) + + render_colorspace = color_management["rendering_space"] + + self.log.info("tx: converting colorspace {0} " + "-> {1}".format(colorspace, render_colorspace)) + args.extend(["--colorconvert", colorspace, render_colorspace]) + args.extend(["--colorconfig", config_path]) + + else: + # We can't rely on the colorspace attribute when not + # in color managed mode because the collected color space + # is the color space attribute of the file node which can be + # any string whatsoever but only appears disabled in Attribute + # Editor. We assume we're always converting to linear/Raw if + # the source file is assumed to be sRGB. + render_colorspace = "linear" + if _has_arnold(): + img_info = image_info(filepath) + color_space = guess_colorspace(img_info) + if color_space.lower() == "sRGB": + self.log.info("tx: converting sRGB -> linear") + args.extend(["--colorconvert", "sRGB", "Raw"]) + else: + self.log.info("tx: texture's colorspace " + "is already linear") + else: + self.log.warning("tx: cannot guess the colorspace, " + "color conversion won't be available!") + + hash_args.append("maketx") + hash_args.extend(args) + + texture_hash = source_hash(filepath, *args) + + if not force_copy: + existing = self._get_existing_hashed_texture(filepath) + if existing: + return existing + + # Exclude these additional arguments from the hashing because + # it is the hash itself + args.extend([ "--sattrib", "sourceHash", texture_hash - ] - if linearize: - if cmds.colorManagementPrefs(query=True, cmEnabled=True): - render_colorspace = cmds.colorManagementPrefs(query=True, - renderingSpaceName=True) # noqa - config_path = cmds.colorManagementPrefs(query=True, - configFilePath=True) # noqa - if not os.path.exists(config_path): - raise RuntimeError("No OCIO config path found!") + ]) - color_space_attr = resource["node"] + ".colorSpace" - try: - color_space = cmds.getAttr(color_space_attr) - except ValueError: - # node doesn't have color space attribute - if _has_arnold(): - img_info = image_info(filepath) - color_space = guess_colorspace(img_info) - else: - color_space = "Raw" - self.log.info("tx: converting {0} -> {1}".format(color_space, render_colorspace)) # noqa - - additional_args.extend(["--colorconvert", - color_space, - render_colorspace]) - else: - - if _has_arnold(): - img_info = image_info(filepath) - color_space = guess_colorspace(img_info) - if color_space == "sRGB": - self.log.info("tx: converting sRGB -> linear") - additional_args.extend(["--colorconvert", - "sRGB", - "Raw"]) - else: - self.log.info("tx: texture's colorspace " - "is already linear") - else: - self.log.warning("cannot guess the colorspace" - "color conversion won't be available!") # noqa - - - additional_args.extend(["--colorconfig", config_path]) # Ensure folder exists if not os.path.exists(os.path.dirname(converted)): os.makedirs(os.path.dirname(converted)) @@ -562,13 +582,20 @@ class ExtractLook(publish.Extractor): maketx( filepath, converted, - additional_args, + args, self.log ) - return converted, COPY, texture_hash + return converted, COPY, texture_hash, render_colorspace - return filepath, COPY, texture_hash + # No special treatment for this file + texture_hash = source_hash(filepath) + if not force_copy: + existing = self._get_existing_hashed_texture(filepath) + if existing: + return existing + + return filepath, COPY, texture_hash, colorspace class ExtractModelRenderSets(ExtractLook): From c7e12b5184b39738d9225504de89054938754720 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 24 Mar 2023 19:39:51 +0100 Subject: [PATCH 144/228] Cosmetics --- openpype/hosts/maya/plugins/publish/extract_look.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 5c03aa5a5a..a067e63339 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -344,8 +344,6 @@ class ExtractLook(publish.Extractor): # didn't do color spaces different than the current # resource. processed_file = processed_files[filepath] - processed_colorspace = processed_file["color_space"] - processed_result_colorspace = processed_file["result_color_space"] self.log.debug( "File was already processed. Likely used by another " "resource too: {}".format(filepath) @@ -355,10 +353,12 @@ class ExtractLook(publish.Extractor): self.log.warning( "File was already processed but using another" "colorspace: {} <-> {}" - "".format(colorspace, processed_colorspace)) + "".format(colorspace, + processed_file["color_space"])) self._set_resource_result_colorspace( - resource, colorspace=processed_result_colorspace + resource, + colorspace=processed_file["result_color_space"] ) continue From 4ea987fb1f442069a3127bdf046c57bf2f8393af Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 25 Mar 2023 03:26:01 +0000 Subject: [PATCH 145/228] [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 5b6db12b5e..bc5ea7fe7c 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.3-nightly.3" +__version__ = "3.15.3-nightly.4" From b37c15f58215bbdb51227e690cf3dde0cd0fdd96 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 25 Mar 2023 10:52:44 +0100 Subject: [PATCH 146/228] More WIP refactoring for TextureProcessors --- .../maya/plugins/publish/extract_look.py | 295 +++++++++--------- 1 file changed, 148 insertions(+), 147 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index b21ce72296..7515fdeb8c 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -26,37 +26,6 @@ COPY = 1 HARDLINK = 2 -def _has_arnold(): - """Return whether the arnold package is available and can be imported.""" - try: - import arnold # noqa: F401 - return True - except (ImportError, ModuleNotFoundError): - return False - - -def get_redshift_tool(tool_name): - """Path to redshift texture processor. - - On Windows it adds .exe extension if missing from tool argument. - - Args: - tool (string): Tool name. - - Returns: - str: Full path to redshift texture processor executable. - """ - redshift_os_path = os.environ["REDSHIFT_COREDATAPATH"] - - redshift_tool_path = os.path.join( - redshift_os_path, - "bin", - tool_name - ) - - return find_executable(redshift_tool_path) - - def find_paths_by_hash(texture_hash): """Find the texture hash key in the dictionary. @@ -75,17 +44,26 @@ def find_paths_by_hash(texture_hash): @six.add_metaclass(ABCMeta) class TextureProcessor: - def __init__(self, log=None): if log is None: log = logging.getLogger(self.__class___.__name__) + self.log = log @abstractmethod - def process(self, filepath): - + def apply_settings(self, system_settings, project_settings): pass + @abstractmethod + def process(self, + source, + colorspace, + color_management, + staging_dir): + pass + + # TODO: Warning this only supports Py3.3+ @staticmethod + @abstractmethod def get_extension(): pass @@ -93,10 +71,11 @@ class TextureProcessor: class MakeRSTexBin(TextureProcessor): """Make `.rstexbin` using `redshiftTextureProcessor`""" - def __init__(self): - super(MakeRSTexBin, self).__init__() - - def process(self, source, *args): + def process(self, + source, + colorspace, + color_management, + staging_dir): """ with some default settings. @@ -105,24 +84,22 @@ class MakeRSTexBin(TextureProcessor): Args: source (str): Path to source file. - *args: Additional arguments for `redshiftTextureProcessor`. """ if "REDSHIFT_COREDATAPATH" not in os.environ: raise RuntimeError("Must have Redshift available.") - texture_processor_path = get_redshift_tool("redshiftTextureProcessor") + texture_processor_path = self.get_redshift_tool( + "redshiftTextureProcessor" + ) if not texture_processor_path: - raise KnownPublishError("Must have Redshift available.", - title="Make RSTexBin texture") + raise KnownPublishError("Must have Redshift available.") subprocess_args = [ texture_processor_path, source ] - subprocess_args.extend(args) - self.log.debug(" ".join(subprocess_args)) try: out = run_subprocess(subprocess_args) @@ -131,18 +108,43 @@ class MakeRSTexBin(TextureProcessor): exc_info=True) raise + # TODO: Implement correct return values return out @staticmethod def get_extension(): return ".rstexbin" + @staticmethod + def get_redshift_tool(tool_name): + """Path to redshift texture processor. + + On Windows it adds .exe extension if missing from tool argument. + + Args: + tool (string): Tool name. + + Returns: + str: Full path to redshift texture processor executable. + """ + redshift_os_path = os.environ["REDSHIFT_COREDATAPATH"] + + redshift_tool_path = os.path.join( + redshift_os_path, + "bin", + tool_name + ) + + return find_executable(redshift_tool_path) + class MakeTX(TextureProcessor): - def __init__(self): - super(MakeTX, self).__init__() - def process(self, source, destination, *args): + def process(self, + source, + colorspace, + color_management, + staging_dir): """Make `.tx` using `maketx` with some default settings. The settings are based on default as used in Arnold's @@ -152,8 +154,6 @@ class MakeTX(TextureProcessor): Args: source (str): Path to source file. - destination (str): Writing destination path. - *args: Additional arguments for `maketx`. Returns: str: Output of `maketx` command. @@ -164,9 +164,78 @@ class MakeTX(TextureProcessor): maketx_path = get_oiio_tools_path("maketx") if not maketx_path: - print( - "OIIO tool not found in {}".format(maketx_path)) - raise AssertionError("OIIO tool not found") + raise AssertionError( + "OIIO 'maketx' tool not found. Result: {}".format(maketx_path) + ) + + # Define .tx filepath in staging if source file is not .tx + fname, ext = os.path.splitext(os.path.basename(source)) + if ext == ".tx": + # TODO: Implement this fallback + # Do nothing if the source file is already a .tx file. + # return source, COPY, texture_hash, render_colorspace + pass + + args = [] + if color_management["enabled"]: + config_path = color_management["config"] + if not os.path.exists(config_path): + raise RuntimeError("OCIO config not found at: " + "{}".format(config_path)) + + render_colorspace = color_management["rendering_space"] + + self.log.info("tx: converting colorspace {0} " + "-> {1}".format(colorspace, + render_colorspace)) + args.extend(["--colorconvert", colorspace, render_colorspace]) + args.extend(["--colorconfig", config_path]) + + else: + # We can't rely on the colorspace attribute when not in color + # managed mode because the collected color space is the color space + # attribute of the file node which can be any string whatsoever + # but only appears disabled in Attribute Editor. We assume we're + # always converting to linear/Raw if the source file is assumed to + # be sRGB. + # TODO Without color management do we even know we can do + # "colorconvert" and what config does that end up using since + # colorconvert is a OCIO command line flag for maketx. + # Also, Raw != linear? + render_colorspace = "linear" + if self._has_arnold(): + img_info = image_info(source) + color_space = guess_colorspace(img_info) + if color_space.lower() == "sRGB": + self.log.info("tx: converting sRGB -> linear") + args.extend(["--colorconvert", "sRGB", "Raw"]) + else: + self.log.info("tx: texture's colorspace " + "is already linear") + else: + self.log.warning("tx: cannot guess the colorspace, " + "color conversion won't be " + "available!") + + args.append("maketx") + args.extend(args) + + texture_hash = source_hash(source, *args) + + # Exclude these additional arguments from the hashing because + # it is the hash itself + args.extend([ + "--sattrib", + "sourceHash", + texture_hash + ]) + + # Ensure folder exists + converted = os.path.join(staging_dir, "resources", fname + ".tx") + if not os.path.exists(os.path.dirname(converted)): + os.makedirs(os.path.dirname(converted)) + + self.log.info("Generating .tx file for %s .." % source) subprocess_args = [ maketx_path, @@ -186,18 +255,27 @@ class MakeTX(TextureProcessor): self.log.debug(" ".join(subprocess_args)) try: - out = run_subprocess(subprocess_args) + run_subprocess(subprocess_args) except Exception: self.log.error("Texture maketx conversion failed", exc_info=True) raise - return out + return converted, COPY, texture_hash, render_colorspace @staticmethod def get_extension(): return ".tx" + @staticmethod + def _has_arnold(): + """Return whether the arnold package is available and importable.""" + try: + import arnold # noqa: F401 + return True + except (ImportError, ModuleNotFoundError): + return False + @contextlib.contextmanager def no_workspace_dir(): @@ -565,7 +643,7 @@ class ExtractLook(publish.Extractor): basename, ext = os.path.splitext(os.path.basename(filepath)) # Get extension from the last processor - for processors in reversed(processors): + for processor in reversed(processors): ext = processor.get_extension() self.log.debug("Processor {} defined extension: " "{}".format(processor, ext)) @@ -620,7 +698,6 @@ class ExtractLook(publish.Extractor): Returns: tuple: (filepath, copy_mode, texture_hash, result_colorspace) """ - fname, ext = os.path.splitext(os.path.basename(filepath)) # Note: The texture hash is only reliable if we include any potential # conversion arguments provide to e.g. `maketx` @@ -629,104 +706,28 @@ class ExtractLook(publish.Extractor): if len(processors) > 1: raise KnownPublishError( - "More than one texture processor not supported" + "More than one texture processor not supported. " + "Current processors enabled: {}".format(processors) ) # TODO: Make all processors take the same arguments for processor in processors: - if processor is MakeTX: - processed_path = processor().process(filepath, - converted, - "--sattrib", - "sourceHash", - escape_space(texture_hash), # noqa - colorconvert, - color_config, - ) - self.log.info("Generating texture file for %s .." % filepath) # noqa - self.log.info(converted) - if processed_path: - return processed_path, COPY, texture_hash - else: - self.log.info("maketx has returned nothing") - elif processor is MakeRSTexBin: - processed_path = processor().process(filepath) - self.log.info("Generating texture file for %s .." % filepath) # noqa - if processed_path: - return processed_path, COPY, texture_hash - else: - self.log.info("redshift texture converter has returned nothing") # noqa + self.log.debug("Processing texture {} with processor {}".format( + filepath, processor + )) - # TODO: continue this refactoring to processor - if do_maketx and ext != ".tx": - # Define .tx filepath in staging if source file is not .tx - converted = os.path.join(staging_dir, "resources", fname + ".tx") + processed_result = processor.process(filepath, + colorspace, + color_management) + if not processed_result: + raise RuntimeError("Texture Processor {} returned " + "no result.".format(processor)) - if color_management["enabled"]: - config_path = color_management["config"] - if not os.path.exists(config_path): - raise RuntimeError("OCIO config not found at: " - "{}".format(config_path)) + processed_path, processed_texture_hash = processed_result + self.log.info("Generated processed " + "texture: {}".format(processed_path)) - render_colorspace = color_management["rendering_space"] - - self.log.info("tx: converting colorspace {0} " - "-> {1}".format(colorspace, render_colorspace)) - args.extend(["--colorconvert", colorspace, render_colorspace]) - args.extend(["--colorconfig", config_path]) - - else: - # We can't rely on the colorspace attribute when not - # in color managed mode because the collected color space - # is the color space attribute of the file node which can be - # any string whatsoever but only appears disabled in Attribute - # Editor. We assume we're always converting to linear/Raw if - # the source file is assumed to be sRGB. - render_colorspace = "linear" - if _has_arnold(): - img_info = image_info(filepath) - color_space = guess_colorspace(img_info) - if color_space.lower() == "sRGB": - self.log.info("tx: converting sRGB -> linear") - args.extend(["--colorconvert", "sRGB", "Raw"]) - else: - self.log.info("tx: texture's colorspace " - "is already linear") - else: - self.log.warning("tx: cannot guess the colorspace, " - "color conversion won't be available!") - - hash_args.append("maketx") - hash_args.extend(args) - - texture_hash = source_hash(filepath, *args) - - if not force_copy: - existing = self._get_existing_hashed_texture(filepath) - if existing: - return existing - - # Exclude these additional arguments from the hashing because - # it is the hash itself - args.extend([ - "--sattrib", - "sourceHash", - texture_hash - ]) - - # Ensure folder exists - if not os.path.exists(os.path.dirname(converted)): - os.makedirs(os.path.dirname(converted)) - - self.log.info("Generating .tx file for %s .." % filepath) - maketx( - filepath, - converted, - args, - self.log - ) - - return converted, COPY, texture_hash, render_colorspace + return processed_path, COPY, processed_texture_hash # No special treatment for this file texture_hash = source_hash(filepath) From a6a392e9640b1028fe5f1e199df5bb0d96c4f570 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 25 Mar 2023 12:32:35 +0100 Subject: [PATCH 147/228] Allow maketx with color management enabled --- .../publish/validate_look_color_space.py | 26 ------------------- 1 file changed, 26 deletions(-) delete mode 100644 openpype/hosts/maya/plugins/publish/validate_look_color_space.py diff --git a/openpype/hosts/maya/plugins/publish/validate_look_color_space.py b/openpype/hosts/maya/plugins/publish/validate_look_color_space.py deleted file mode 100644 index b1bdeb7541..0000000000 --- a/openpype/hosts/maya/plugins/publish/validate_look_color_space.py +++ /dev/null @@ -1,26 +0,0 @@ -from maya import cmds - -import pyblish.api -from openpype.pipeline.publish import ValidateContentsOrder -from openpype.pipeline import PublishValidationError - - -class ValidateMayaColorSpace(pyblish.api.InstancePlugin): - """ - Check if the OCIO Color Management and maketx options - enabled at the same time - """ - - order = ValidateContentsOrder - families = ['look'] - hosts = ['maya'] - label = 'Color Management with maketx' - - def process(self, instance): - ocio_maya = cmds.colorManagementPrefs(q=True, - cmConfigFileEnabled=True, - cmEnabled=True) - maketx = instance.data["maketx"] - - if ocio_maya and maketx: - raise PublishValidationError("Maya is color managed and maketx option is on. OpenPype doesn't support this combination yet.") # noqa From 6b841253d7e33d194db4e4cd3804952a819631b9 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 25 Mar 2023 14:26:22 +0100 Subject: [PATCH 148/228] Fix hash args --- openpype/hosts/maya/plugins/publish/extract_look.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 7515fdeb8c..c8339a1335 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -217,10 +217,9 @@ class MakeTX(TextureProcessor): "color conversion won't be " "available!") - args.append("maketx") - args.extend(args) - - texture_hash = source_hash(source, *args) + hash_args = ["maketx"] + hash_args.extend(args) + texture_hash = source_hash(source, *hash_args) # Exclude these additional arguments from the hashing because # it is the hash itself From 44c0009e728921076bbfdad59fca0b834261b01c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 25 Mar 2023 14:37:33 +0100 Subject: [PATCH 149/228] Allow to configure extra arguments in OP settings for `maketx` --- .../maya/plugins/publish/extract_look.py | 34 ++++++++++++++++--- .../defaults/project_settings/maya.json | 3 ++ .../schemas/schema_maya_publish.json | 16 +++++++++ 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index c8339a1335..2951261cd6 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -139,13 +139,31 @@ class MakeRSTexBin(TextureProcessor): class MakeTX(TextureProcessor): + """Make `.tx` using `maketx` with some default settings.""" + + def __init__(self, log=None): + super(MakeTX, self).__init__(log=log) + self.extra_args = [] + + def apply_settings(self, system_settings, project_settings): + # Allow extra maketx arguments from project settings + extra_args_dict = ( + project_settings["maya"]["publish"] + .get("ExtractLook", {}) + .get("maketx_arguments", {}) + ) + extra_args = [] + for flag, value in extra_args_dict.items(): + extra_args.append(flag) + extra_args.append(value) + self.extra_args = extra_args def process(self, source, colorspace, color_management, staging_dir): - """Make `.tx` using `maketx` with some default settings. + """ The settings are based on default as used in Arnold's txManager in the scene. @@ -154,6 +172,10 @@ class MakeTX(TextureProcessor): Args: source (str): Path to source file. + colorspace (str): Colorspace of the source file. + color_management (dict): Maya Color management data from + `lib.get_color_management_preferences` + staging_dir (str): Output directory to write to. Returns: str: Output of `maketx` command. @@ -230,9 +252,9 @@ class MakeTX(TextureProcessor): ]) # Ensure folder exists - converted = os.path.join(staging_dir, "resources", fname + ".tx") - if not os.path.exists(os.path.dirname(converted)): - os.makedirs(os.path.dirname(converted)) + destination = os.path.join(staging_dir, "resources", fname + ".tx") + if not os.path.exists(os.path.dirname(destination)): + os.makedirs(os.path.dirname(destination)) self.log.info("Generating .tx file for %s .." % source) @@ -250,6 +272,8 @@ class MakeTX(TextureProcessor): ] subprocess_args.extend(args) + if self.extra_args: + subprocess_args.extend(self.extra_args) subprocess_args.extend(["-o", destination]) self.log.debug(" ".join(subprocess_args)) @@ -260,7 +284,7 @@ class MakeTX(TextureProcessor): exc_info=True) raise - return converted, COPY, texture_hash, render_colorspace + return destination, COPY, texture_hash, render_colorspace @staticmethod def get_extension(): diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index ef327fbd6b..502dbda870 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -925,6 +925,9 @@ "enabled": true, "active": true, "ogsfx_path": "/maya2glTF/PBR/shaders/glTF_PBR.ogsfx" + }, + "ExtractLook": { + "maketx_arguments": {} } }, "load": { diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json index 5a66f8a513..da12dde6b2 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json @@ -996,6 +996,22 @@ "label": "GLSL Shader Directory" } ] + }, + { + "type": "dict", + "collapsible": true, + "key": "ExtractLook", + "label": "Extract Look", + "children": [ + { + "key": "maketx_arguments", + "label": "Extra arguments for maketx command line", + "type": "dict-modifiable", + "object_type": { + "type": "text" + } + } + ] } ] } From c181d3ff2e14a3797a274ce9e90c1c08f21cac5f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 25 Mar 2023 14:54:19 +0100 Subject: [PATCH 150/228] Intialize texture processors with applied settings --- .../maya/plugins/publish/extract_look.py | 36 +++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 2951261cd6..9777f14f11 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -402,6 +402,25 @@ class ExtractLook(publish.Extractor): self.log.info("No sets found") return + # Specify texture processing executables to activate + # TODO: Load these more dynamically once we support more processors + processors = [] + context = instance.context + for key, Processor in { + # Instance data key to texture processor mapping + "maketx": MakeTX, + "rstex": MakeRSTexBin + }.items(): + if instance.data.get(key, False): + processor = Processor() + processor.apply_settings(context.data["system_settings"], + context.data["project_settings"]) + processors.append(processor) + + if processors: + self.log.debug("Collected texture processors: " + "{}".format(processors)) + self.log.debug("Processing resources..") results = self.process_resources(instance, staging_dir=dir_path) transfers = results["fileTransfers"] @@ -499,7 +518,7 @@ class ExtractLook(publish.Extractor): ) resource["result_colorspace"] = colorspace - def process_resources(self, instance, staging_dir): + def process_resources(self, instance, staging_dir, processors): """Process all resources in the instance. It is assumed that all resources are nodes using file textures. @@ -512,7 +531,6 @@ class ExtractLook(publish.Extractor): """ resources = instance.data["resources"] - do_maketx = instance.data.get("maketx", False) color_management = lib.get_color_management_preferences() # Temporary fix to NOT create hardlinks on windows machines @@ -532,14 +550,6 @@ class ExtractLook(publish.Extractor): destinations = {} remap = OrderedDict() - # Specify texture processing executables to activate - processors = [] - if instance.data.get("maketx", False): - processors.append(MakeTX) - # Option to convert textures to native redshift textures - if instance.data.get("rstex", False): - processors.append(MakeRSTexBin) - for resource in resources: colorspace = resource["color_space"] @@ -724,9 +734,6 @@ class ExtractLook(publish.Extractor): # Note: The texture hash is only reliable if we include any potential # conversion arguments provide to e.g. `maketx` - args = [] - hash_args = [] - if len(processors) > 1: raise KnownPublishError( "More than one texture processor not supported. " @@ -741,7 +748,8 @@ class ExtractLook(publish.Extractor): processed_result = processor.process(filepath, colorspace, - color_management) + color_management, + staging_dir) if not processed_result: raise RuntimeError("Texture Processor {} returned " "no result.".format(processor)) From e83708a3fb5da5710142578a4d433643ef529c89 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 25 Mar 2023 14:54:52 +0100 Subject: [PATCH 151/228] Cosmetics --- openpype/hosts/maya/plugins/publish/extract_look.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 9777f14f11..632c64014f 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -149,8 +149,7 @@ class MakeTX(TextureProcessor): # Allow extra maketx arguments from project settings extra_args_dict = ( project_settings["maya"]["publish"] - .get("ExtractLook", {}) - .get("maketx_arguments", {}) + .get("ExtractLook", {}).get("maketx_arguments", {}) ) extra_args = [] for flag, value in extra_args_dict.items(): From d8c763c191f46448cfdbbc17c8939f1c6bf7f9fd Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 25 Mar 2023 14:56:15 +0100 Subject: [PATCH 152/228] Include extra args in maketx texture hash --- openpype/hosts/maya/plugins/publish/extract_look.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 632c64014f..41377bb8f6 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -238,8 +238,11 @@ class MakeTX(TextureProcessor): "color conversion won't be " "available!") + # Note: The texture hash is only reliable if we include any potential + # conversion arguments provide to e.g. `maketx` hash_args = ["maketx"] hash_args.extend(args) + hash_args.extend(self.extra_args) texture_hash = source_hash(source, *hash_args) # Exclude these additional arguments from the hashing because @@ -731,8 +734,6 @@ class ExtractLook(publish.Extractor): tuple: (filepath, copy_mode, texture_hash, result_colorspace) """ - # Note: The texture hash is only reliable if we include any potential - # conversion arguments provide to e.g. `maketx` if len(processors) > 1: raise KnownPublishError( "More than one texture processor not supported. " From ba1d0c570ab4a20417a450dd2c5c030ac3f4901d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 25 Mar 2023 15:01:03 +0100 Subject: [PATCH 153/228] Fix arguments --- openpype/hosts/maya/plugins/publish/extract_look.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 41377bb8f6..77cf0d8a83 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -424,7 +424,9 @@ class ExtractLook(publish.Extractor): "{}".format(processors)) self.log.debug("Processing resources..") - results = self.process_resources(instance, staging_dir=dir_path) + results = self.process_resources(instance, + staging_dir=dir_path, + processors=processors) transfers = results["fileTransfers"] hardlinks = results["fileHardlinks"] hashes = results["fileHashes"] From 0fa5eab7c904ab497274c37c6d0a8dfc38015f36 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 25 Mar 2023 20:29:49 +0100 Subject: [PATCH 154/228] Implement TextureResult dataclass + fix publishing with and without `maketx` --- .../maya/plugins/publish/extract_look.py | 106 ++++++++++++------ 1 file changed, 72 insertions(+), 34 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 77cf0d8a83..b166e85e7a 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -10,6 +10,7 @@ import tempfile import platform import contextlib from collections import OrderedDict +import attr from maya import cmds # noqa @@ -26,6 +27,19 @@ COPY = 1 HARDLINK = 2 +@attr.s +class TextureResult: + # Path to the file + path = attr.ib() + # Colorspace of the resulting texture. This might not be the input + # colorspace of the texture if a TextureProcessor has processed the file. + colorspace = attr.ib() + # Hash generated for the texture using openpype.lib.source_hash + file_hash = attr.ib() + # The transfer mode, e.g. COPY or HARDLINK + transfer_mode = attr.ib() + + def find_paths_by_hash(texture_hash): """Find the texture hash key in the dictionary. @@ -46,7 +60,7 @@ def find_paths_by_hash(texture_hash): class TextureProcessor: def __init__(self, log=None): if log is None: - log = logging.getLogger(self.__class___.__name__) + log = logging.getLogger(self.__class__.__name__) self.log = log @abstractmethod @@ -67,6 +81,10 @@ class TextureProcessor: def get_extension(): pass + def __repr__(self): + # Log instance as class name + return self.__class__.__name__ + class MakeRSTexBin(TextureProcessor): """Make `.rstexbin` using `redshiftTextureProcessor`""" @@ -100,6 +118,9 @@ class MakeRSTexBin(TextureProcessor): source ] + hash_args = ["rstex"] + texture_hash = source_hash(source, *hash_args) + self.log.debug(" ".join(subprocess_args)) try: out = run_subprocess(subprocess_args) @@ -108,8 +129,12 @@ class MakeRSTexBin(TextureProcessor): exc_info=True) raise - # TODO: Implement correct return values - return out + return TextureResult( + path=out, + file_hash=texture_hash, + colorspace=colorspace, + transfer_mode=COPY + ) @staticmethod def get_extension(): @@ -192,10 +217,13 @@ class MakeTX(TextureProcessor): # Define .tx filepath in staging if source file is not .tx fname, ext = os.path.splitext(os.path.basename(source)) if ext == ".tx": - # TODO: Implement this fallback # Do nothing if the source file is already a .tx file. - # return source, COPY, texture_hash, render_colorspace - pass + return TextureResult( + path=source, + file_hash=None, # todo: unknown texture hash? + colorspace=colorspace, + transfer_mode=COPY + ) args = [] if color_management["enabled"]: @@ -286,7 +314,12 @@ class MakeTX(TextureProcessor): exc_info=True) raise - return destination, COPY, texture_hash, render_colorspace + return TextureResult( + path=destination, + file_hash=texture_hash, + colorspace=render_colorspace, + transfer_mode=COPY + ) @staticmethod def get_extension(): @@ -510,17 +543,17 @@ class ExtractLook(publish.Extractor): def _set_resource_result_colorspace(self, resource, colorspace): """Update resource resulting colorspace after texture processing""" - if "result_colorspace" in resource: - if resource["result_colorspace"] == colorspace: + if "result_color_space" in resource: + if resource["result_color_space"] == colorspace: return self.log.warning( "Resource already has a resulting colorspace but is now " "being overridden to a new one: {} -> {}".format( - resource["result_colorspace"], colorspace + resource["result_color_space"], colorspace ) ) - resource["result_colorspace"] = colorspace + resource["result_color_space"] = colorspace def process_resources(self, instance, staging_dir, processors): """Process all resources in the instance. @@ -592,37 +625,33 @@ class ExtractLook(publish.Extractor): color_management=color_management, colorspace=colorspace ) - source, mode, texture_hash, result_colorspace = texture_result + source = texture_result.path destination = self.resource_destination(instance, - source, + texture_result.path, processors) # Set the resulting color space on the resource self._set_resource_result_colorspace( - resource, colorspace=result_colorspace + resource, colorspace=texture_result.colorspace ) processed_files[filepath] = { "color_space": colorspace, - "result_color_space": result_colorspace, + "result_color_space": texture_result.colorspace, } - # Force copy is specified. - if force_copy: - mode = COPY - - if mode == COPY: + if force_copy or texture_result.transfer_mode == COPY: transfers.append((source, destination)) self.log.info('file will be copied {} -> {}'.format( source, destination)) - elif mode == HARDLINK: + elif texture_result.transfer_mode == HARDLINK: hardlinks.append((source, destination)) self.log.info('file will be hardlinked {} -> {}'.format( source, destination)) # Store the hashes from hash to destination to include in the # database - hashes[texture_hash] = destination + hashes[texture_result.file_hash] = destination source = os.path.normpath(resource["source"]) if source not in destinations: @@ -697,10 +726,9 @@ class ExtractLook(publish.Extractor): # then don't reprocess but hardlink from the original existing = find_paths_by_hash(texture_hash) if existing: - self.log.info("Found hash in database, preparing hardlink..") source = next((p for p in existing if os.path.exists(p)), None) if source: - return source, HARDLINK, texture_hash + return source else: self.log.warning( "Paths not found on disk, " @@ -722,7 +750,7 @@ class ExtractLook(publish.Extractor): Args: filepath (str): The source file path to process. - processors (list): List of TexProcessor processing the texture + processors (list): List of TextureProcessor processing the texture staging_dir (str): The staging directory to write to. force_copy (bool): Whether to force a copy even if a file hash might have existed already in the project, otherwise @@ -733,7 +761,7 @@ class ExtractLook(publish.Extractor): texture belongs to. Returns: - tuple: (filepath, copy_mode, texture_hash, result_colorspace) + TextureResult: The texture result information. """ if len(processors) > 1: @@ -742,7 +770,6 @@ class ExtractLook(publish.Extractor): "Current processors enabled: {}".format(processors) ) - # TODO: Make all processors take the same arguments for processor in processors: self.log.debug("Processing texture {} with processor {}".format( filepath, processor @@ -755,21 +782,32 @@ class ExtractLook(publish.Extractor): if not processed_result: raise RuntimeError("Texture Processor {} returned " "no result.".format(processor)) - - processed_path, processed_texture_hash = processed_result self.log.info("Generated processed " - "texture: {}".format(processed_path)) + "texture: {}".format(processed_result.path)) - return processed_path, COPY, processed_texture_hash + # TODO: Currently all processors force copy instead of allowing + # hardlinks using source hashes. This should be refactored + return processed_result - # No special treatment for this file + # No texture processing for this file texture_hash = source_hash(filepath) if not force_copy: existing = self._get_existing_hashed_texture(filepath) if existing: - return existing + self.log.info("Found hash in database, preparing hardlink..") + return TextureResult( + path=filepath, + file_hash=texture_hash, + colorspace=colorspace, + transfer_mode=HARDLINK + ) - return filepath, COPY, texture_hash, colorspace + return TextureResult( + path=filepath, + file_hash=texture_hash, + colorspace=colorspace, + transfer_mode=COPY + ) class ExtractModelRenderSets(ExtractLook): From 81b5d771270b27849ee322d09c06e9ee75cd4f73 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 25 Mar 2023 20:42:34 +0100 Subject: [PATCH 155/228] Cleanup, fix Py2 compatibility --- .../maya/plugins/publish/extract_look.py | 47 ++++++++++++++----- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index b166e85e7a..dd59cd7dcc 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -63,7 +63,6 @@ class TextureProcessor: log = logging.getLogger(self.__class__.__name__) self.log = log - @abstractmethod def apply_settings(self, system_settings, project_settings): pass @@ -73,11 +72,28 @@ class TextureProcessor: colorspace, color_management, staging_dir): + """Process the `source` texture. + + Must be implemented on inherited class. + + This must always return a TextureResult even when it does not generate + a texture. If it doesn't generate a texture then it should return a + TextureResult using the input path and colorspace. + + Args: + source (str): Path to source file. + colorspace (str): Colorspace of the source file. + color_management (dict): Maya Color management data from + `lib.get_color_management_preferences` + staging_dir (str): Output directory to write to. + + Returns: + TextureResult: The resulting texture information. + + """ pass - # TODO: Warning this only supports Py3.3+ @staticmethod - @abstractmethod def get_extension(): pass @@ -164,7 +180,12 @@ class MakeRSTexBin(TextureProcessor): class MakeTX(TextureProcessor): - """Make `.tx` using `maketx` with some default settings.""" + """Make `.tx` using `maketx` with some default settings. + + Some hardcoded arguments passed to `maketx` are based on the defaults used + in Arnold's txManager tool. + + """ def __init__(self, log=None): super(MakeTX, self).__init__(log=log) @@ -187,12 +208,10 @@ class MakeTX(TextureProcessor): colorspace, color_management, staging_dir): - """ + """Process the texture. - The settings are based on default as used in Arnold's - txManager in the scene. This function requires the `maketx` executable to be - on the `PATH`. + available in the OIIO tool. Args: source (str): Path to source file. @@ -202,7 +221,7 @@ class MakeTX(TextureProcessor): staging_dir (str): Output directory to write to. Returns: - str: Output of `maketx` command. + TextureResult: The resulting texture information. """ from openpype.lib import get_oiio_tools_path @@ -474,7 +493,7 @@ class ExtractLook(publish.Extractor): # To avoid Maya trying to automatically remap the file # textures relative to the `workspace -directory` we force # it to a fake temporary workspace. This fixes textures - # getting incorrectly remapped. (LKD-17, PLN-101) + # getting incorrectly remapped. with no_workspace_dir(): with lib.attribute_values(remap): with lib.maintained_selection(): @@ -710,9 +729,11 @@ class ExtractLook(publish.Extractor): # Get extension from the last processor for processor in reversed(processors): - ext = processor.get_extension() - self.log.debug("Processor {} defined extension: " - "{}".format(processor, ext)) + processor_ext = processor.get_extension() + if processor_ext: + self.log.debug("Processor {} defined extension: " + "{}".format(processor, ext)) + ext = processor_ext break return os.path.join( From 7e92f2184e52f2b75f8f85e930bb8974f8e4289c Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Sun, 26 Mar 2023 09:43:27 +0100 Subject: [PATCH 156/228] Default playblast compression to PNG --- openpype/settings/defaults/project_settings/maya.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 4a97ea8a09..e914eb29f9 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -790,7 +790,7 @@ "ExtractPlayblast": { "capture_preset": { "Codec": { - "compression": "jpg", + "compression": "png", "format": "image", "quality": 95 }, From 16e5bb630f015f7deaa9a86e592cfe8f34fb94ac Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 26 Mar 2023 20:30:35 +0200 Subject: [PATCH 157/228] Cleanup --- openpype/hosts/maya/plugins/publish/extract_look.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index dd59cd7dcc..2368295bf4 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -441,8 +441,6 @@ class ExtractLook(publish.Extractor): dir_path = self.staging_dir(instance) maya_fname = "{0}.{1}".format(instance.name, self.scene_type) json_fname = "{0}.{1}".format(instance.name, self.look_data_type) - - # Make texture dump folder maya_path = os.path.join(dir_path, maya_fname) json_path = os.path.join(dir_path, json_fname) From b340f6a57ca0ae9c22c29e0e2efebe23e8685df8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 26 Mar 2023 20:51:10 +0200 Subject: [PATCH 158/228] Clarify more about the issue in logging message + minor cleanup --- .../hosts/maya/plugins/publish/extract_look.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 2368295bf4..b68d8ae545 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -623,10 +623,14 @@ class ExtractLook(publish.Extractor): if colorspace != processed_file["color_space"]: self.log.warning( - "File was already processed but using another" - "colorspace: {} <-> {}" - "".format(colorspace, - processed_file["color_space"])) + "File '{}' was already processed using colorspace " + "'{}' instead of the current resource's " + "colorspace '{}'. The already processed texture " + "result's colorspace '{}' will be used." + "".format(filepath, + colorspace, + processed_file["color_space"], + processed_file["result_color_space"])) self._set_resource_result_colorspace( resource, @@ -642,7 +646,6 @@ class ExtractLook(publish.Extractor): color_management=color_management, colorspace=colorspace ) - source = texture_result.path destination = self.resource_destination(instance, texture_result.path, processors) @@ -657,6 +660,7 @@ class ExtractLook(publish.Extractor): "result_color_space": texture_result.colorspace, } + source = texture_result.path if force_copy or texture_result.transfer_mode == COPY: transfers.append((source, destination)) self.log.info('file will be copied {} -> {}'.format( From 81e25eb3a30a4ad801f09ff528458f5ba309e4fb Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 26 Mar 2023 21:06:50 +0200 Subject: [PATCH 159/228] Move caching into one place --- .../maya/plugins/publish/extract_look.py | 39 +++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index b68d8ae545..2e268749a0 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -596,14 +596,23 @@ class ExtractLook(publish.Extractor): else: force_copy = instance.data.get("forceCopy", False) + destinations_cache = {} + + def get_resource_destination_cached(path): + """Get resource destination with cached result per filepath""" + if path not in destinations_cache: + self.get_resource_destination(path, + instance.data["resourcesDir"], + processors) + destinations_cache[path] = destination + return destinations_cache[path] + # Process all resource's individual files processed_files = {} transfers = [] hardlinks = [] hashes = {} - destinations = {} remap = OrderedDict() - for resource in resources: colorspace = resource["color_space"] @@ -646,9 +655,6 @@ class ExtractLook(publish.Extractor): color_management=color_management, colorspace=colorspace ) - destination = self.resource_destination(instance, - texture_result.path, - processors) # Set the resulting color space on the resource self._set_resource_result_colorspace( @@ -661,6 +667,7 @@ class ExtractLook(publish.Extractor): } source = texture_result.path + destination = get_resource_destination_cached(source) if force_copy or texture_result.transfer_mode == COPY: transfers.append((source, destination)) self.log.info('file will be copied {} -> {}'.format( @@ -674,14 +681,6 @@ class ExtractLook(publish.Extractor): # database hashes[texture_result.file_hash] = destination - source = os.path.normpath(resource["source"]) - if source not in destinations: - # Cache destination as source resource might be included - # multiple times - destinations[source] = self.resource_destination( - instance, source, processors - ) - # Set up remapping attributes for the node during the publish # The order of these can be important if one attribute directly # affects another, e.g. we set colorspace after filepath because @@ -690,7 +689,9 @@ class ExtractLook(publish.Extractor): # attributes changed in the resulting publish) # Remap filepath to publish destination filepath_attr = resource["attribute"] - remap[filepath_attr] = destinations[source] + remap[filepath_attr] = get_resource_destination_cached( + resource["source"] + ) # Preserve color space values (force value after filepath change) # This will also trigger in the same order at end of context to @@ -709,23 +710,21 @@ class ExtractLook(publish.Extractor): "attrRemap": remap, } - def resource_destination(self, instance, filepath, processors): + def get_resource_destination(self, filepath, resources_dir, processors): """Get resource destination path. This is utility function to change path if resource file name is changed by some external tool like `maketx`. Args: - instance: Current Instance. - filepath (str): Resource path - processor: Texture processors converting resource. + filepath (str): Resource source path + resources_dir (str): Destination dir for resources in publish. + processors (list): Texture processors converting resource. Returns: str: Path to resource file """ - resources_dir = instance.data["resourcesDir"] - # Compute destination location basename, ext = os.path.splitext(os.path.basename(filepath)) From 03e5ff92ea3d6d229d7b91640852eead7628a356 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 26 Mar 2023 21:07:38 +0200 Subject: [PATCH 160/228] Fix typo --- openpype/hosts/maya/plugins/publish/extract_look.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 2e268749a0..f846b4ee3d 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -601,9 +601,8 @@ class ExtractLook(publish.Extractor): def get_resource_destination_cached(path): """Get resource destination with cached result per filepath""" if path not in destinations_cache: - self.get_resource_destination(path, - instance.data["resourcesDir"], - processors) + destination = self.get_resource_destination( + path, instance.data["resourcesDir"], processors) destinations_cache[path] = destination return destinations_cache[path] From 2917ee27750384a3c952ced17e257fa99d48aeb8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 26 Mar 2023 21:10:57 +0200 Subject: [PATCH 161/228] Fix logging --- openpype/hosts/maya/plugins/publish/extract_look.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index f846b4ee3d..d5d8da04e7 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -730,9 +730,11 @@ class ExtractLook(publish.Extractor): # Get extension from the last processor for processor in reversed(processors): processor_ext = processor.get_extension() - if processor_ext: - self.log.debug("Processor {} defined extension: " - "{}".format(processor, ext)) + if processor_ext and ext != processor_ext: + self.log.debug("Processor {} overrides extension to '{}' " + "for path: {}".format(processor, + processor_ext, + filepath)) ext = processor_ext break From 00e5f220f4fd664ecd00cc7cae4fefab6984f89c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 26 Mar 2023 21:12:37 +0200 Subject: [PATCH 162/228] Add todo --- openpype/hosts/maya/plugins/publish/extract_look.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index d5d8da04e7..33ba4cc7a3 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -687,6 +687,11 @@ class ExtractLook(publish.Extractor): # filepaths (which is avoidable, but we don't want to have those # attributes changed in the resulting publish) # Remap filepath to publish destination + # TODO It would be much better if we could use the destination path + # from the actual processed texture results, but since the + # attribute will need to preserve tokens like , etc for + # now we will define the output path from the attribute value + # including the tokens to persist them. filepath_attr = resource["attribute"] remap[filepath_attr] = get_resource_destination_cached( resource["source"] From 09e0ba54f24c73a721f6f4d31ce23c3d786178c6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 27 Mar 2023 09:57:37 +0200 Subject: [PATCH 163/228] TVPaint: Optional Validation plugins can be de/activated by user (#4674) * fix TVPaint validation plugins so they can be enabled/disabled * add missing 'is_active' method usage * added option to enable auto detet render creator * added fps to instance data * safe access to layers * fix handling of missing render layer * render passes are safer * fix convertor plugin --- .../tvpaint/plugins/create/convert_legacy.py | 4 ++-- .../hosts/tvpaint/plugins/create/create_render.py | 15 ++++++++------- .../plugins/publish/collect_instance_frames.py | 2 ++ .../plugins/publish/validate_asset_name.py | 12 ++++++++++-- .../plugins/publish/validate_layers_visibility.py | 2 +- .../tvpaint/plugins/publish/validate_marks.py | 13 +++++++++++-- .../plugins/publish/validate_scene_settings.py | 13 +++++++++++-- .../plugins/publish/validate_start_frame.py | 13 +++++++++++-- .../defaults/project_settings/tvpaint.json | 1 + .../projects_schema/schema_project_tvpaint.json | 6 ++++++ 10 files changed, 63 insertions(+), 18 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/create/convert_legacy.py b/openpype/hosts/tvpaint/plugins/create/convert_legacy.py index 538c6e4c5e..5cfa1faa50 100644 --- a/openpype/hosts/tvpaint/plugins/create/convert_legacy.py +++ b/openpype/hosts/tvpaint/plugins/create/convert_legacy.py @@ -55,7 +55,7 @@ class TVPaintLegacyConverted(SubsetConvertorPlugin): self._convert_render_layers( to_convert["renderLayer"], current_instances) self._convert_render_passes( - to_convert["renderpass"], current_instances) + to_convert["renderPass"], current_instances) self._convert_render_scenes( to_convert["renderScene"], current_instances) self._convert_workfiles( @@ -116,7 +116,7 @@ class TVPaintLegacyConverted(SubsetConvertorPlugin): render_layers_by_group_id = {} for instance in current_instances: if instance.get("creator_identifier") == "render.layer": - group_id = instance["creator_identifier"]["group_id"] + group_id = instance["creator_attributes"]["group_id"] render_layers_by_group_id[group_id] = instance for render_pass in render_passes: diff --git a/openpype/hosts/tvpaint/plugins/create/create_render.py b/openpype/hosts/tvpaint/plugins/create/create_render.py index 9711024c79..2369c7329f 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render.py @@ -415,11 +415,11 @@ class CreateRenderPass(TVPaintCreator): .get("creator_attributes", {}) .get("render_layer_instance_id") ) - render_layer_info = render_layers.get(render_layer_instance_id) + render_layer_info = render_layers.get(render_layer_instance_id, {}) self.update_instance_labels( instance_data, - render_layer_info["variant"], - render_layer_info["template_data"] + render_layer_info.get("variant"), + render_layer_info.get("template_data") ) instance = CreatedInstance.from_existing(instance_data, self) self._add_instance_to_context(instance) @@ -607,11 +607,11 @@ class CreateRenderPass(TVPaintCreator): current_instances = self.host.list_instances() render_layers = [ { - "value": instance["instance_id"], - "label": instance["subset"] + "value": inst["instance_id"], + "label": inst["subset"] } - for instance in current_instances - if instance["creator_identifier"] == CreateRenderlayer.identifier + for inst in current_instances + if inst.get("creator_identifier") == CreateRenderlayer.identifier ] if not render_layers: render_layers.append({"value": None, "label": "N/A"}) @@ -697,6 +697,7 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): ["create"] ["auto_detect_render"] ) + self.enabled = plugin_settings.get("enabled", False) self.allow_group_rename = plugin_settings["allow_group_rename"] self.group_name_template = plugin_settings["group_name_template"] self.group_idx_offset = plugin_settings["group_idx_offset"] diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_instance_frames.py b/openpype/hosts/tvpaint/plugins/publish/collect_instance_frames.py index 5eb702a1da..63f04cf3ce 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_instance_frames.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_instance_frames.py @@ -22,9 +22,11 @@ class CollectOutputFrameRange(pyblish.api.InstancePlugin): context = instance.context frame_start = asset_doc["data"]["frameStart"] + fps = asset_doc["data"]["fps"] frame_end = frame_start + ( context.data["sceneMarkOut"] - context.data["sceneMarkIn"] ) + instance.data["fps"] = fps instance.data["frameStart"] = frame_start instance.data["frameEnd"] = frame_end self.log.info( diff --git a/openpype/hosts/tvpaint/plugins/publish/validate_asset_name.py b/openpype/hosts/tvpaint/plugins/publish/validate_asset_name.py index 7e35726030..d7984ce971 100644 --- a/openpype/hosts/tvpaint/plugins/publish/validate_asset_name.py +++ b/openpype/hosts/tvpaint/plugins/publish/validate_asset_name.py @@ -1,5 +1,8 @@ import pyblish.api -from openpype.pipeline import PublishXmlValidationError +from openpype.pipeline import ( + PublishXmlValidationError, + OptionalPyblishPluginMixin, +) from openpype.hosts.tvpaint.api.pipeline import ( list_instances, write_instances, @@ -31,7 +34,10 @@ class FixAssetNames(pyblish.api.Action): write_instances(new_instance_items) -class ValidateAssetNames(pyblish.api.ContextPlugin): +class ValidateAssetName( + OptionalPyblishPluginMixin, + pyblish.api.ContextPlugin +): """Validate assset name present on instance. Asset name on instance should be the same as context's. @@ -43,6 +49,8 @@ class ValidateAssetNames(pyblish.api.ContextPlugin): actions = [FixAssetNames] def process(self, context): + if not self.is_active(context.data): + return context_asset_name = context.data["asset"] for instance in context: asset_name = instance.data.get("asset") diff --git a/openpype/hosts/tvpaint/plugins/publish/validate_layers_visibility.py b/openpype/hosts/tvpaint/plugins/publish/validate_layers_visibility.py index 6a496a2e49..8e52a636f4 100644 --- a/openpype/hosts/tvpaint/plugins/publish/validate_layers_visibility.py +++ b/openpype/hosts/tvpaint/plugins/publish/validate_layers_visibility.py @@ -11,7 +11,7 @@ class ValidateLayersVisiblity(pyblish.api.InstancePlugin): families = ["review", "render"] def process(self, instance): - layers = instance.data["layers"] + layers = instance.data.get("layers") # Instance have empty layers # - it is not job of this validator to check that if not layers: diff --git a/openpype/hosts/tvpaint/plugins/publish/validate_marks.py b/openpype/hosts/tvpaint/plugins/publish/validate_marks.py index 0030b0fd1c..7b2cc62bb5 100644 --- a/openpype/hosts/tvpaint/plugins/publish/validate_marks.py +++ b/openpype/hosts/tvpaint/plugins/publish/validate_marks.py @@ -1,7 +1,10 @@ import json import pyblish.api -from openpype.pipeline import PublishXmlValidationError +from openpype.pipeline import ( + PublishXmlValidationError, + OptionalPyblishPluginMixin, +) from openpype.hosts.tvpaint.api.lib import execute_george @@ -23,7 +26,10 @@ class ValidateMarksRepair(pyblish.api.Action): ) -class ValidateMarks(pyblish.api.ContextPlugin): +class ValidateMarks( + OptionalPyblishPluginMixin, + pyblish.api.ContextPlugin +): """Validate mark in and out are enabled and it's duration. Mark In/Out does not have to match frameStart and frameEnd but duration is @@ -59,6 +65,9 @@ class ValidateMarks(pyblish.api.ContextPlugin): } def process(self, context): + if not self.is_active(context.data): + return + current_data = { "markIn": context.data["sceneMarkIn"], "markInState": context.data["sceneMarkInState"], diff --git a/openpype/hosts/tvpaint/plugins/publish/validate_scene_settings.py b/openpype/hosts/tvpaint/plugins/publish/validate_scene_settings.py index 4473e4b1b7..0ab8e811f5 100644 --- a/openpype/hosts/tvpaint/plugins/publish/validate_scene_settings.py +++ b/openpype/hosts/tvpaint/plugins/publish/validate_scene_settings.py @@ -1,11 +1,17 @@ import json import pyblish.api -from openpype.pipeline import PublishXmlValidationError +from openpype.pipeline import ( + PublishXmlValidationError, + OptionalPyblishPluginMixin, +) # TODO @iLliCiTiT add fix action for fps -class ValidateProjectSettings(pyblish.api.ContextPlugin): +class ValidateProjectSettings( + OptionalPyblishPluginMixin, + pyblish.api.ContextPlugin +): """Validate scene settings against database.""" label = "Validate Scene Settings" @@ -13,6 +19,9 @@ class ValidateProjectSettings(pyblish.api.ContextPlugin): optional = True def process(self, context): + if not self.is_active(context.data): + return + expected_data = context.data["assetEntity"]["data"] scene_data = { "fps": context.data.get("sceneFps"), diff --git a/openpype/hosts/tvpaint/plugins/publish/validate_start_frame.py b/openpype/hosts/tvpaint/plugins/publish/validate_start_frame.py index 066e54c670..229ccfcd18 100644 --- a/openpype/hosts/tvpaint/plugins/publish/validate_start_frame.py +++ b/openpype/hosts/tvpaint/plugins/publish/validate_start_frame.py @@ -1,5 +1,8 @@ import pyblish.api -from openpype.pipeline import PublishXmlValidationError +from openpype.pipeline import ( + PublishXmlValidationError, + OptionalPyblishPluginMixin, +) from openpype.hosts.tvpaint.api.lib import execute_george @@ -14,7 +17,10 @@ class RepairStartFrame(pyblish.api.Action): execute_george("tv_startframe 0") -class ValidateStartFrame(pyblish.api.ContextPlugin): +class ValidateStartFrame( + OptionalPyblishPluginMixin, + pyblish.api.ContextPlugin +): """Validate start frame being at frame 0.""" label = "Validate Start Frame" @@ -24,6 +30,9 @@ class ValidateStartFrame(pyblish.api.ContextPlugin): optional = True def process(self, context): + if not self.is_active(context.data): + return + start_frame = execute_george("tv_startframe") if start_frame == 0: return diff --git a/openpype/settings/defaults/project_settings/tvpaint.json b/openpype/settings/defaults/project_settings/tvpaint.json index 9173a8c3d5..1671748e97 100644 --- a/openpype/settings/defaults/project_settings/tvpaint.json +++ b/openpype/settings/defaults/project_settings/tvpaint.json @@ -42,6 +42,7 @@ "default_variants": [] }, "auto_detect_render": { + "enabled": false, "allow_group_rename": true, "group_name_template": "L{group_index}", "group_idx_offset": 10, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json index 708b688ba5..1094595851 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json @@ -202,7 +202,13 @@ "key": "auto_detect_render", "label": "Auto-Detect Create Render", "is_group": true, + "checkbox_key": "enabled", "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, { "type": "label", "label": "The creator tries to auto-detect Render Layers and Render Passes in scene. For Render Layers is used group name as a variant and for Render Passes is used TVPaint layer name.

    Group names can be renamed by their used order in scene. The renaming template where can be used {group_index} formatting key which is filled by \"used position index of group\".
    - Template: L{group_index}
    - Group offset: 10
    - Group padding: 3
    Would create group names \"L010\", \"L020\", ..." From 35f76a2365d76aa97ca32a5591e831662dcd4029 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 27 Mar 2023 10:20:02 +0200 Subject: [PATCH 164/228] Fix Redshift .rstexbin support --- openpype/hosts/maya/plugins/publish/extract_look.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 33ba4cc7a3..46eb940879 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -137,16 +137,21 @@ class MakeRSTexBin(TextureProcessor): hash_args = ["rstex"] texture_hash = source_hash(source, *hash_args) + # Redshift stores the output texture next to the input but with + # the extension replaced to `.rstexbin + basename, ext = os.path.splitext(source) + destination = "{}{}".format(basename, self.get_extension()) + self.log.debug(" ".join(subprocess_args)) try: - out = run_subprocess(subprocess_args) + run_subprocess(subprocess_args) except Exception: self.log.error("Texture .rstexbin conversion failed", exc_info=True) raise return TextureResult( - path=out, + path=destination, file_hash=texture_hash, colorspace=colorspace, transfer_mode=COPY From 95fcce48bd3a2476a89bf2823fb1abcaa8b1d056 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 27 Mar 2023 10:22:33 +0200 Subject: [PATCH 165/228] Move `REDSHIFT_COREDATAPATH` environment check to `get_redshift_tool` --- openpype/hosts/maya/plugins/publish/extract_look.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 46eb940879..ad9ba34224 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -120,9 +120,6 @@ class MakeRSTexBin(TextureProcessor): source (str): Path to source file. """ - if "REDSHIFT_COREDATAPATH" not in os.environ: - raise RuntimeError("Must have Redshift available.") - texture_processor_path = self.get_redshift_tool( "redshiftTextureProcessor" ) @@ -173,10 +170,11 @@ class MakeRSTexBin(TextureProcessor): Returns: str: Full path to redshift texture processor executable. """ - redshift_os_path = os.environ["REDSHIFT_COREDATAPATH"] + if "REDSHIFT_COREDATAPATH" not in os.environ: + raise RuntimeError("Must have Redshift available.") redshift_tool_path = os.path.join( - redshift_os_path, + os.environ["REDSHIFT_COREDATAPATH"], "bin", tool_name ) From 9773cbbedc5224b130badee8a734d090c76aee33 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 27 Mar 2023 10:27:13 +0200 Subject: [PATCH 166/228] Cleanup --- .../maya/plugins/publish/extract_look.py | 92 ++++++++++--------- 1 file changed, 47 insertions(+), 45 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index ad9ba34224..4e04999e7e 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -29,6 +29,7 @@ HARDLINK = 2 @attr.s class TextureResult: + """The resulting texture of a processed file for a resource""" # Path to the file path = attr.ib() # Colorspace of the resulting texture. This might not be the input @@ -56,6 +57,38 @@ def find_paths_by_hash(texture_hash): return legacy_io.distinct(key, {"type": "version"}) +@contextlib.contextmanager +def no_workspace_dir(): + """Force maya to a fake temporary workspace directory. + + Note: This is not maya.cmds.workspace 'rootDirectory' but the 'directory' + + This helps to avoid Maya automatically remapping image paths to files + relative to the currently set directory. + + """ + + # Store current workspace + original = cmds.workspace(query=True, directory=True) + + # Set a fake workspace + fake_workspace_dir = tempfile.mkdtemp() + cmds.workspace(directory=fake_workspace_dir) + + try: + yield + finally: + try: + cmds.workspace(directory=original) + except RuntimeError: + # If the original workspace directory didn't exist either + # ignore the fact that it fails to reset it to the old path + pass + + # Remove the temporary directory + os.rmdir(fake_workspace_dir) + + @six.add_metaclass(ABCMeta) class TextureProcessor: def __init__(self, log=None): @@ -64,6 +97,16 @@ class TextureProcessor: self.log = log def apply_settings(self, system_settings, project_settings): + """Apply OpenPype system/project settings to the TextureProcessor + + Args: + system_settings (dict): OpenPype system settings + project_settings (dict): OpenPype project settings + + Returns: + None + + """ pass @abstractmethod @@ -110,16 +153,7 @@ class MakeRSTexBin(TextureProcessor): colorspace, color_management, staging_dir): - """ - with some default settings. - This function requires the `REDSHIFT_COREDATAPATH` - to be in `PATH`. - - Args: - source (str): Path to source file. - - """ texture_processor_path = self.get_redshift_tool( "redshiftTextureProcessor" ) @@ -135,7 +169,7 @@ class MakeRSTexBin(TextureProcessor): texture_hash = source_hash(source, *hash_args) # Redshift stores the output texture next to the input but with - # the extension replaced to `.rstexbin + # the extension replaced to `.rstexbin` basename, ext = os.path.splitext(source) destination = "{}{}".format(basename, self.get_extension()) @@ -165,7 +199,7 @@ class MakeRSTexBin(TextureProcessor): On Windows it adds .exe extension if missing from tool argument. Args: - tool (string): Tool name. + tool_name (string): Tool name. Returns: str: Full path to redshift texture processor executable. @@ -213,8 +247,8 @@ class MakeTX(TextureProcessor): staging_dir): """Process the texture. - This function requires the `maketx` executable to be - available in the OIIO tool. + This function requires the `maketx` executable to be available in an + OpenImageIO toolset detectable by OpenPype. Args: source (str): Path to source file. @@ -357,38 +391,6 @@ class MakeTX(TextureProcessor): return False -@contextlib.contextmanager -def no_workspace_dir(): - """Force maya to a fake temporary workspace directory. - - Note: This is not maya.cmds.workspace 'rootDirectory' but the 'directory' - - This helps to avoid Maya automatically remapping image paths to files - relative to the currently set directory. - - """ - - # Store current workspace - original = cmds.workspace(query=True, directory=True) - - # Set a fake workspace - fake_workspace_dir = tempfile.mkdtemp() - cmds.workspace(directory=fake_workspace_dir) - - try: - yield - finally: - try: - cmds.workspace(directory=original) - except RuntimeError: - # If the original workspace directory didn't exist either - # ignore the fact that it fails to reset it to the old path - pass - - # Remove the temporary directory - os.rmdir(fake_workspace_dir) - - class ExtractLook(publish.Extractor): """Extract Look (Maya Scene + JSON) From 52511c1e3f8924c89a100cdbe2f2d2c34923ef4f Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Mon, 27 Mar 2023 10:07:54 +0100 Subject: [PATCH 167/228] Update openpype/scripts/otio_burnin.py Co-authored-by: Roy Nieterau --- openpype/scripts/otio_burnin.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/openpype/scripts/otio_burnin.py b/openpype/scripts/otio_burnin.py index afae4f9e08..1f151b5dc6 100644 --- a/openpype/scripts/otio_burnin.py +++ b/openpype/scripts/otio_burnin.py @@ -42,7 +42,22 @@ SOURCE_TIMECODE_KEY = "{source_timecode}" def convert_list_to_commands(list_to_convert, fps, label=""): - path = None + """Convert a list of values to a drawtext command file for ffmpeg `sendcmd` + + The list of values is expected to have a value per frame. If the video + file ends up being longer than the amount of samples per frame than the + last value will be held. + + Args: + list_to_convert (list): List of values per frame. + fps (float or int): The expected frame per seconds of the output file + label (str): Label for the + + Returns: + str: Filepath to the temporary drawtext command file. + + """ + with tempfile.NamedTemporaryFile(mode="w", delete=False) as f: for i, value in enumerate(list_to_convert): seconds = i / fps @@ -61,9 +76,7 @@ def convert_list_to_commands(list_to_convert, fps, label=""): f.write(line) f.flush() - path = f.name - - return path + return f.name def _get_ffprobe_data(source): From 28a4f09625f6dc7f9aad7c8cd6a26bfe25c4f370 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 27 Mar 2023 10:10:21 +0100 Subject: [PATCH 168/228] Finish method description --- openpype/scripts/otio_burnin.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/scripts/otio_burnin.py b/openpype/scripts/otio_burnin.py index 1f151b5dc6..ca0930537c 100644 --- a/openpype/scripts/otio_burnin.py +++ b/openpype/scripts/otio_burnin.py @@ -50,8 +50,9 @@ def convert_list_to_commands(list_to_convert, fps, label=""): Args: list_to_convert (list): List of values per frame. - fps (float or int): The expected frame per seconds of the output file - label (str): Label for the + fps (float or int): The expected frame per seconds of the output file. + label (str): Label for the drawtext, if specific drawtext filter is + required Returns: str: Filepath to the temporary drawtext command file. From eaca4f5551032ebea9a0790c331109ce88005290 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 27 Mar 2023 10:10:31 +0100 Subject: [PATCH 169/228] Change method name --- openpype/scripts/otio_burnin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/scripts/otio_burnin.py b/openpype/scripts/otio_burnin.py index ca0930537c..57a1dcfaff 100644 --- a/openpype/scripts/otio_burnin.py +++ b/openpype/scripts/otio_burnin.py @@ -41,7 +41,7 @@ TIMECODE_KEY = "{timecode}" SOURCE_TIMECODE_KEY = "{source_timecode}" -def convert_list_to_commands(list_to_convert, fps, label=""): +def convert_list_to_command(list_to_convert, fps, label=""): """Convert a list of values to a drawtext command file for ffmpeg `sendcmd` The list of values is expected to have a value per frame. If the video @@ -627,7 +627,7 @@ def burnins_from_data( if list_to_convert: value = list_to_convert[0] - path = convert_list_to_commands( + path = convert_list_to_command( list_to_convert, data["fps"], label=align ) cmd = "sendcmd=f='{}'".format(path) From a178ca1569a43c1becfba3db0209ba3f982d4959 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 27 Mar 2023 17:35:45 +0800 Subject: [PATCH 170/228] remove duplicated imported function --- openpype/hosts/max/api/menu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/api/menu.py b/openpype/hosts/max/api/menu.py index b52acc3c38..066cc90039 100644 --- a/openpype/hosts/max/api/menu.py +++ b/openpype/hosts/max/api/menu.py @@ -5,7 +5,7 @@ from pymxs import runtime as rt from openpype.tools.utils import host_tools from openpype.hosts.max.api import lib -from openpype.hosts.max.api import lib + class OpenPypeMenu(object): """Object representing OpenPype menu. From 7f4fe957bc43d6290b63cdc86d4b4844fcd435c4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 27 Mar 2023 12:02:44 +0200 Subject: [PATCH 171/228] Move `get_oiio_tools_path` import to top of file --- openpype/hosts/maya/plugins/publish/extract_look.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 4e04999e7e..e0869b73e6 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -17,7 +17,7 @@ from maya import cmds # noqa import pyblish.api from openpype.lib.vendor_bin_utils import find_executable -from openpype.lib import source_hash, run_subprocess +from openpype.lib import source_hash, run_subprocess, get_oiio_tools_path from openpype.pipeline import legacy_io, publish, KnownPublishError from openpype.hosts.maya.api import lib from openpype.hosts.maya.api.lib import image_info, guess_colorspace @@ -261,7 +261,6 @@ class MakeTX(TextureProcessor): TextureResult: The resulting texture information. """ - from openpype.lib import get_oiio_tools_path maketx_path = get_oiio_tools_path("maketx") From 39d68780670c2333c0d39ef083331723b501c28d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 27 Mar 2023 12:30:31 +0200 Subject: [PATCH 172/228] Support flags without values for extra `maketx` arguments - Add todo for hardcoded maketx arguments - Add sourceHash argument at end to increase readability of the log --- .../maya/plugins/publish/extract_look.py | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index e0869b73e6..41d34a7ead 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -236,8 +236,16 @@ class MakeTX(TextureProcessor): ) extra_args = [] for flag, value in extra_args_dict.items(): + if not flag: + self.log.debug("Ignoring empty flag from `maketx_arguments` " + "setting..") + continue + extra_args.append(flag) - extra_args.append(value) + if value.strip(): + # There might be flags without values like --opaque-detect + extra_args.append(value) + self.extra_args = extra_args def process(self, @@ -328,14 +336,6 @@ class MakeTX(TextureProcessor): hash_args.extend(self.extra_args) texture_hash = source_hash(source, *hash_args) - # Exclude these additional arguments from the hashing because - # it is the hash itself - args.extend([ - "--sattrib", - "sourceHash", - texture_hash - ]) - # Ensure folder exists destination = os.path.join(staging_dir, "resources", fname + ".tx") if not os.path.exists(os.path.dirname(destination)): @@ -347,9 +347,12 @@ class MakeTX(TextureProcessor): maketx_path, "-v", # verbose "-u", # update mode + # --checknan doesn't influence the output file but aborts the + # conversion if it finds any. So we can avoid need + "--checknan", + # todo: --unpremult, --oiio, --filter should be in the file hash # unpremultiply before conversion (recommended when alpha present) "--unpremult", - "--checknan", # use oiio-optimized settings for tile-size, planarconfig, metadata "--oiio", "--filter", "lanczos3", @@ -359,6 +362,15 @@ class MakeTX(TextureProcessor): subprocess_args.extend(args) if self.extra_args: subprocess_args.extend(self.extra_args) + + # Add source hash attribute after other arguments for log readability + # Note: argument is excluding from the hash since it is the hash itself + subprocess_args.extend([ + "--sattrib", + "sourceHash", + texture_hash + ]) + subprocess_args.extend(["-o", destination]) self.log.debug(" ".join(subprocess_args)) From db27765637f0213cab4d4d2b8a89d2fb2f147ecd Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 27 Mar 2023 12:31:01 +0200 Subject: [PATCH 173/228] Grammar --- openpype/hosts/maya/plugins/publish/extract_look.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 41d34a7ead..f3d0790e22 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -364,7 +364,7 @@ class MakeTX(TextureProcessor): subprocess_args.extend(self.extra_args) # Add source hash attribute after other arguments for log readability - # Note: argument is excluding from the hash since it is the hash itself + # Note: argument is excluded from the hash since it is the hash itself subprocess_args.extend([ "--sattrib", "sourceHash", From 7dfaa5b4f4cbf4b8e821d0b4af2e78bc5d7bee97 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 27 Mar 2023 12:36:59 +0200 Subject: [PATCH 174/228] Add maketx hardcoded flags to the file hash --- .../hosts/maya/plugins/publish/extract_look.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index f3d0790e22..be3de13b37 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -288,7 +288,15 @@ class MakeTX(TextureProcessor): transfer_mode=COPY ) - args = [] + # Hardcoded default arguments for maketx conversion based on Arnold's + # txManager in Maya + args = [ + # unpremultiply before conversion (recommended when alpha present) + "--unpremult", + # use oiio-optimized settings for tile-size, planarconfig, metadata + "--oiio", + "--filter", "lanczos3", + ] if color_management["enabled"]: config_path = color_management["config"] if not os.path.exists(config_path): @@ -348,14 +356,8 @@ class MakeTX(TextureProcessor): "-v", # verbose "-u", # update mode # --checknan doesn't influence the output file but aborts the - # conversion if it finds any. So we can avoid need + # conversion if it finds any. So we can avoid it for the file hash "--checknan", - # todo: --unpremult, --oiio, --filter should be in the file hash - # unpremultiply before conversion (recommended when alpha present) - "--unpremult", - # use oiio-optimized settings for tile-size, planarconfig, metadata - "--oiio", - "--filter", "lanczos3", source ] From b9d1d560a78950142a145588ea46ed54d03415dd Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 27 Mar 2023 12:59:53 +0200 Subject: [PATCH 175/228] :bug: pass zoom settings if review is attached --- openpype/hosts/maya/plugins/publish/collect_review.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/maya/plugins/publish/collect_review.py b/openpype/hosts/maya/plugins/publish/collect_review.py index 548b1c996a..5d5ec294b1 100644 --- a/openpype/hosts/maya/plugins/publish/collect_review.py +++ b/openpype/hosts/maya/plugins/publish/collect_review.py @@ -79,6 +79,7 @@ class CollectReview(pyblish.api.InstancePlugin): data['review_width'] = instance.data['review_width'] data['review_height'] = instance.data['review_height'] data["isolate"] = instance.data["isolate"] + data["panZoom"] = instance.data.get("panZoom", False) cmds.setAttr(str(instance) + '.active', 1) self.log.debug('data {}'.format(instance.context[i].data)) instance.context[i].data.update(data) From 5b4b0e601b0f2a4adefc56ba2251a487e4818253 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 27 Mar 2023 13:12:36 +0200 Subject: [PATCH 176/228] :bug: pass panel too --- openpype/hosts/maya/plugins/publish/collect_review.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/maya/plugins/publish/collect_review.py b/openpype/hosts/maya/plugins/publish/collect_review.py index 5d5ec294b1..71c65cc47e 100644 --- a/openpype/hosts/maya/plugins/publish/collect_review.py +++ b/openpype/hosts/maya/plugins/publish/collect_review.py @@ -80,6 +80,7 @@ class CollectReview(pyblish.api.InstancePlugin): data['review_height'] = instance.data['review_height'] data["isolate"] = instance.data["isolate"] data["panZoom"] = instance.data.get("panZoom", False) + data["panel"] =instance.data["panel"] cmds.setAttr(str(instance) + '.active', 1) self.log.debug('data {}'.format(instance.context[i].data)) instance.context[i].data.update(data) From 5805e7d90041938da22eb63844679db433415d62 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 27 Mar 2023 13:13:09 +0200 Subject: [PATCH 177/228] :rotating_light: add space --- openpype/hosts/maya/plugins/publish/collect_review.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_review.py b/openpype/hosts/maya/plugins/publish/collect_review.py index 71c65cc47e..36affe852b 100644 --- a/openpype/hosts/maya/plugins/publish/collect_review.py +++ b/openpype/hosts/maya/plugins/publish/collect_review.py @@ -80,7 +80,7 @@ class CollectReview(pyblish.api.InstancePlugin): data['review_height'] = instance.data['review_height'] data["isolate"] = instance.data["isolate"] data["panZoom"] = instance.data.get("panZoom", False) - data["panel"] =instance.data["panel"] + data["panel"] = instance.data["panel"] cmds.setAttr(str(instance) + '.active', 1) self.log.debug('data {}'.format(instance.context[i].data)) instance.context[i].data.update(data) From 3444660a982b13cf798b575e9ebecf8f85f49f05 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 27 Mar 2023 13:20:26 +0200 Subject: [PATCH 178/228] Convert to "linear" because it's always available in OIIO if OCIO is disabled or no valid config is found - If OCIO is not enabled (or cannot find a valid configuration, OIIO will at least be able to convert among linear, sRGB, and Rec709.) --- openpype/hosts/maya/plugins/publish/extract_look.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index be3de13b37..5b9b0777a0 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -316,19 +316,15 @@ class MakeTX(TextureProcessor): # managed mode because the collected color space is the color space # attribute of the file node which can be any string whatsoever # but only appears disabled in Attribute Editor. We assume we're - # always converting to linear/Raw if the source file is assumed to + # always converting to linear if the source file is assumed to # be sRGB. - # TODO Without color management do we even know we can do - # "colorconvert" and what config does that end up using since - # colorconvert is a OCIO command line flag for maketx. - # Also, Raw != linear? render_colorspace = "linear" if self._has_arnold(): img_info = image_info(source) color_space = guess_colorspace(img_info) if color_space.lower() == "sRGB": self.log.info("tx: converting sRGB -> linear") - args.extend(["--colorconvert", "sRGB", "Raw"]) + args.extend(["--colorconvert", "sRGB", render_colorspace]) else: self.log.info("tx: texture's colorspace " "is already linear") From 6f015d6d64278b696f861b0b85284ed9e4ca9417 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 27 Mar 2023 13:31:33 +0200 Subject: [PATCH 179/228] Cleanup/refactor based on @fabiaserra comments --- .../maya/plugins/publish/extract_look.py | 39 +++++++++---------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 5b9b0777a0..daf6735660 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -91,6 +91,9 @@ def no_workspace_dir(): @six.add_metaclass(ABCMeta) class TextureProcessor: + + extension = None + def __init__(self, log=None): if log is None: log = logging.getLogger(self.__class__.__name__) @@ -136,10 +139,6 @@ class TextureProcessor: """ pass - @staticmethod - def get_extension(): - pass - def __repr__(self): # Log instance as class name return self.__class__.__name__ @@ -148,6 +147,8 @@ class TextureProcessor: class MakeRSTexBin(TextureProcessor): """Make `.rstexbin` using `redshiftTextureProcessor`""" + extension = ".rstexbin" + def process(self, source, colorspace, @@ -171,7 +172,7 @@ class MakeRSTexBin(TextureProcessor): # Redshift stores the output texture next to the input but with # the extension replaced to `.rstexbin` basename, ext = os.path.splitext(source) - destination = "{}{}".format(basename, self.get_extension()) + destination = "{}{}".format(basename, self.extension) self.log.debug(" ".join(subprocess_args)) try: @@ -188,10 +189,6 @@ class MakeRSTexBin(TextureProcessor): transfer_mode=COPY ) - @staticmethod - def get_extension(): - return ".rstexbin" - @staticmethod def get_redshift_tool(tool_name): """Path to redshift texture processor. @@ -224,6 +221,8 @@ class MakeTX(TextureProcessor): """ + extension = ".tx" + def __init__(self, log=None): super(MakeTX, self).__init__(log=log) self.extra_args = [] @@ -335,15 +334,13 @@ class MakeTX(TextureProcessor): # Note: The texture hash is only reliable if we include any potential # conversion arguments provide to e.g. `maketx` - hash_args = ["maketx"] - hash_args.extend(args) - hash_args.extend(self.extra_args) + hash_args = ["maketx"] + args + self.extra_args texture_hash = source_hash(source, *hash_args) # Ensure folder exists - destination = os.path.join(staging_dir, "resources", fname + ".tx") - if not os.path.exists(os.path.dirname(destination)): - os.makedirs(os.path.dirname(destination)) + resources_dir = os.path.join(staging_dir, "resources") + if not os.path.exists(resources_dir): + os.makedirs(resources_dir) self.log.info("Generating .tx file for %s .." % source) @@ -369,6 +366,7 @@ class MakeTX(TextureProcessor): texture_hash ]) + destination = os.path.join(resources_dir, fname + ".tx") subprocess_args.extend(["-o", destination]) self.log.debug(" ".join(subprocess_args)) @@ -386,10 +384,6 @@ class MakeTX(TextureProcessor): transfer_mode=COPY ) - @staticmethod - def get_extension(): - return ".tx" - @staticmethod def _has_arnold(): """Return whether the arnold package is available and importable.""" @@ -748,7 +742,7 @@ class ExtractLook(publish.Extractor): # Get extension from the last processor for processor in reversed(processors): - processor_ext = processor.get_extension() + processor_ext = processor.extension if processor_ext and ext != processor_ext: self.log.debug("Processor {} overrides extension to '{}' " "for path: {}".format(processor, @@ -785,9 +779,12 @@ class ExtractLook(publish.Extractor): color_management, colorspace): """Process a single texture file on disk for publishing. + This will: 1. Check whether it's already published, if so it will do hardlink - 2. If not published and maketx is enabled, generate a new .tx file. + (if the texture hash is found and force copy is not enabled) + 2. It will process the texture using the supplied texture + processors like MakeTX and MakeRSTexBin if enabled. 3. Compute the destination path for the source file. Args: From e11d1f279ac03da71158075b1793b7b673303cab Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 27 Mar 2023 13:46:33 +0200 Subject: [PATCH 180/228] Add assumption for some file formats to be sRGB: `.png`, `.jpeg` and `.jpg` --- .../maya/plugins/publish/extract_look.py | 36 +++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index daf6735660..9c360c8dd4 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -26,6 +26,10 @@ from openpype.hosts.maya.api.lib import image_info, guess_colorspace COPY = 1 HARDLINK = 2 +# File formats we will assume are sRGB when Maya Color Management is disabled +# Currently only used for `maketx` color conversion logic +NONLINEAR_FILE_FORMATS = {".png", ".jpeg", ".jpg"} + @attr.s class TextureResult: @@ -311,6 +315,7 @@ class MakeTX(TextureProcessor): args.extend(["--colorconfig", config_path]) else: + self.log.debug("Maya color management is disabled..") # We can't rely on the colorspace attribute when not in color # managed mode because the collected color space is the color space # attribute of the file node which can be any string whatsoever @@ -318,19 +323,28 @@ class MakeTX(TextureProcessor): # always converting to linear if the source file is assumed to # be sRGB. render_colorspace = "linear" - if self._has_arnold(): + assumed_input_colorspace = "linear" + if ext.lower() in NONLINEAR_FILE_FORMATS: + assumed_input_colorspace = "sRGB" + elif self._has_arnold(): + # Assume colorspace based on input image bit-depth img_info = image_info(source) - color_space = guess_colorspace(img_info) - if color_space.lower() == "sRGB": - self.log.info("tx: converting sRGB -> linear") - args.extend(["--colorconvert", "sRGB", render_colorspace]) - else: - self.log.info("tx: texture's colorspace " - "is already linear") + assumed_input_colorspace = guess_colorspace(img_info) else: - self.log.warning("tx: cannot guess the colorspace, " - "color conversion won't be " - "available!") + self.log.warning("tx: cannot guess the colorspace, a linear " + "colorspace will be assumed for file: " + "{}".format(source)) + + if assumed_input_colorspace == "sRGB": + self.log.info("tx: converting sRGB -> linear") + args.extend(["--colorconvert", "sRGB", render_colorspace]) + elif assumed_input_colorspace == "linear": + self.log.info("tx: texture's colorspace " + "is already linear") + else: + self.log.warning("Unexpected texture color space: {} " + "(expected either 'linear' or 'sRGB')" + "".format(assumed_input_colorspace)) # Note: The texture hash is only reliable if we include any potential # conversion arguments provide to e.g. `maketx` From bf60e7370453831d4ee3f89a32fd435091450ef3 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 27 Mar 2023 13:47:26 +0200 Subject: [PATCH 181/228] Remove `.png` from nonlinear formats assumption because apparently they can be 32-bit --- openpype/hosts/maya/plugins/publish/extract_look.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 9c360c8dd4..c68ce56fcc 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -28,7 +28,7 @@ HARDLINK = 2 # File formats we will assume are sRGB when Maya Color Management is disabled # Currently only used for `maketx` color conversion logic -NONLINEAR_FILE_FORMATS = {".png", ".jpeg", ".jpg"} +NONLINEAR_FILE_FORMATS = {".jpeg", ".jpg"} @attr.s From 5ed6c29eee6531eaa5dde107a98a777ce091f232 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 27 Mar 2023 15:23:14 +0200 Subject: [PATCH 182/228] Mimic Arnold tx manager behavior whenever maya color management is disabled - Do no color conversion when color management is disabled --- .../maya/plugins/publish/extract_look.py | 42 +++++-------------- 1 file changed, 11 insertions(+), 31 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index c68ce56fcc..7405eb1a9f 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -315,36 +315,10 @@ class MakeTX(TextureProcessor): args.extend(["--colorconfig", config_path]) else: - self.log.debug("Maya color management is disabled..") - # We can't rely on the colorspace attribute when not in color - # managed mode because the collected color space is the color space - # attribute of the file node which can be any string whatsoever - # but only appears disabled in Attribute Editor. We assume we're - # always converting to linear if the source file is assumed to - # be sRGB. - render_colorspace = "linear" - assumed_input_colorspace = "linear" - if ext.lower() in NONLINEAR_FILE_FORMATS: - assumed_input_colorspace = "sRGB" - elif self._has_arnold(): - # Assume colorspace based on input image bit-depth - img_info = image_info(source) - assumed_input_colorspace = guess_colorspace(img_info) - else: - self.log.warning("tx: cannot guess the colorspace, a linear " - "colorspace will be assumed for file: " - "{}".format(source)) - - if assumed_input_colorspace == "sRGB": - self.log.info("tx: converting sRGB -> linear") - args.extend(["--colorconvert", "sRGB", render_colorspace]) - elif assumed_input_colorspace == "linear": - self.log.info("tx: texture's colorspace " - "is already linear") - else: - self.log.warning("Unexpected texture color space: {} " - "(expected either 'linear' or 'sRGB')" - "".format(assumed_input_colorspace)) + # Maya Color management is disabled. We cannot rely on an OCIO + self.log.debug("tx: Maya color management is disabled. No color " + "conversion will be applied to .tx conversion for: " + "{}".format(source)) # Note: The texture hash is only reliable if we include any potential # conversion arguments provide to e.g. `maketx` @@ -383,9 +357,15 @@ class MakeTX(TextureProcessor): destination = os.path.join(resources_dir, fname + ".tx") subprocess_args.extend(["-o", destination]) + # We want to make sure we are explicit about what OCIO config gets + # used. So when we supply no --colorconfig flag that no fallback to + # an OCIO env var occurs. + env = os.environ.copy() + env.pop("OCIO", None) + self.log.debug(" ".join(subprocess_args)) try: - run_subprocess(subprocess_args) + run_subprocess(subprocess_args, env=env) except Exception: self.log.error("Texture maketx conversion failed", exc_info=True) From 2cb03b75b74ee145dba5f28b90f09861fe7b350a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 27 Mar 2023 15:35:03 +0200 Subject: [PATCH 183/228] Clean up imports a bit. --- .../maya/plugins/publish/extract_look.py | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 7405eb1a9f..e939992454 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -1,26 +1,24 @@ # -*- coding: utf-8 -*- """Maya look extractor.""" -import logging from abc import ABCMeta, abstractmethod - -import six -import os -import json -import tempfile -import platform -import contextlib from collections import OrderedDict +import contextlib +import json +import logging +import os +import platform +import tempfile +import six import attr -from maya import cmds # noqa - import pyblish.api +from maya import cmds # noqa + from openpype.lib.vendor_bin_utils import find_executable from openpype.lib import source_hash, run_subprocess, get_oiio_tools_path from openpype.pipeline import legacy_io, publish, KnownPublishError from openpype.hosts.maya.api import lib -from openpype.hosts.maya.api.lib import image_info, guess_colorspace # Modes for transfer COPY = 1 From 108bcd8f27acd3d6b632c9df969357f1cee9773b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 27 Mar 2023 15:37:27 +0200 Subject: [PATCH 184/228] Fix missing variable declaration --- openpype/hosts/maya/plugins/publish/extract_look.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index e939992454..9599f9f809 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -317,6 +317,8 @@ class MakeTX(TextureProcessor): self.log.debug("tx: Maya color management is disabled. No color " "conversion will be applied to .tx conversion for: " "{}".format(source)) + # Assume linear + render_colorspace = "linear" # Note: The texture hash is only reliable if we include any potential # conversion arguments provide to e.g. `maketx` From 22dbc4e4fa5227db80fda932f4b9b10a76ba64bd Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 27 Mar 2023 15:40:33 +0200 Subject: [PATCH 185/228] Remove unused variable `NONLINEAR_FILE_FORMATS` --- openpype/hosts/maya/plugins/publish/extract_look.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 9599f9f809..1e339542d7 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -24,10 +24,6 @@ from openpype.hosts.maya.api import lib COPY = 1 HARDLINK = 2 -# File formats we will assume are sRGB when Maya Color Management is disabled -# Currently only used for `maketx` color conversion logic -NONLINEAR_FILE_FORMATS = {".jpeg", ".jpg"} - @attr.s class TextureResult: From 2244a634b25a4191349db3d994c909359bed9c86 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 27 Mar 2023 16:06:52 +0200 Subject: [PATCH 186/228] change minimum frame start/end to '0' (#4719) --- openpype/tools/project_manager/project_manager/view.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index b35491c5b2..2cf11b702d 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -72,8 +72,8 @@ class HierarchyView(QtWidgets.QTreeView): column_delegate_defs = { "name": NameDef(), "type": TypeDef(), - "frameStart": NumberDef(1), - "frameEnd": NumberDef(1), + "frameStart": NumberDef(0), + "frameEnd": NumberDef(0), "fps": NumberDef(1, decimals=3, step=1), "resolutionWidth": NumberDef(0), "resolutionHeight": NumberDef(0), From 0778dd3ec763aa2257f235b310041aed18902b30 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Mon, 27 Mar 2023 15:28:21 +0000 Subject: [PATCH 187/228] [Automated] Release --- CHANGELOG.md | 668 ++++++++++++++++++++++++++++++++++++++++++++ openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 670 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 145c2e2c1a..78ea53f3d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,674 @@ # Changelog +[Full Changelog](https://github.com/ynput/OpenPype/compare/3.15.2...3.15.3) + +### **🆕 New features** + + +

    +Data Exchanges: Point Cloud for 3dsMax #4532 + +Publish PRT format with tyFlow in 3dsmax + +Publish PRT format with tyFlow in 3dsmax and possibly set up loader to load the format too. +- [x] creator +- [x] extractor +- [x] validator +- [x] loader + + +___ + +
    + + +
    +MaxScene Family #4615 + +Introduction of the Max Scene Family + + +___ + +
    + + +
    +Blender: Extract Review #3616 + +Added Review to Blender. + +This implementation is based on #3508 but made compatible for the current implementation of OpenPype for Blender. + + +___ + +
    + + +
    +Global: persistent staging directory for renders #4583 + +Allows configure if staging directory (`stagingDir`) should be persistent with use of profiles. + +With this feature, users can specify a transient data folder path based on presets, which can be used during the creation and publishing stages. In some cases, these DCCs automatically add a rendering path during the creation stage, which is then used in publishing.One of the key advantages of this feature is that it allows users to take advantage of faster storages for rendering, which can help improve workflow efficiency. Additionally, this feature allows users to keep their rendered data persistent, and use their own infrastructure for regular cleaning.However, it should be noted that some productions may want to use this feature without persistency. Furthermore, there may be a need for retargeting the rendering folder to faster storages, which is also not supported at the moment.It is studio responsibility to clean up obsolete folders with data.Location of the folder is configured in `project_anatomy/templates/others`. ('transient' key is expected, with 'folder' key, could be more templates)Which family/task type/subset is applicable is configured in:`project_settings/global/tools/publish/transient_dir_profiles` + + +___ + +
    + +### **🚀 Enhancements** + + +
    +Maya: Multiple values on single render attribute - OP-4131 #4631 + +When validating render attributes, this adds support for multiple values. When repairing first value in list is used. + + +___ + +
    + + +
    +Maya: enable 2D Pan/Zoom for playblasts - OP-5213 #4687 + +Setting for enabling 2D Pan/Zoom on reviews. + + +___ + +
    + + +
    +Houdini: Create button open new publisher's "create" tab #4601 + +During a talk with @maxpareschi he mentioned that the new publisher in Houdini felt super confusing due to "Create" going to the older creator but now being completely empty and the publish button directly went to the publish tab.This resolves that by fixing the Create button to now open the new publisher but on the Create tab.Also made publish button enforce going to the "publish" tab for consistency in usage.@antirotor I think changing the Create button's callback was just missed in this commit or was there a specific reason to not change that around yet? + + +___ + +
    + + +
    +Resolution settings referenced from DB record for 3dsMax #4652 + +- Add Callback for setting the resolution according to DB after the new scene is created. +- Add a new Action into openpype menu which allows the user to reset the resolution in 3dsMax + + +___ + +
    + + +
    +scene length setting referenced from DB record for 3dsMax #4665 + +Setting the timeline length based on DB record in 3dsMax Hosts + + +___ + +
    + + +
    +increment workfile version 3dsmax #4685 + +increment workfile version in 3dsmax as if in blender and maya hosts. + + +___ + +
    + + +
    +Copy existing or generate new Fusion profile on prelaunch #4572 + +Fusion preferences will be copied to the predefined `~/.openpype/hosts/fusion/prefs` folder (or any other folder set in system settings) on launch. + +The idea is to create a copy of existing Fusion profile, adding an OpenPype menu to the Fusion instance.By default the copy setting is turned off, so no file copying is performed. Instead the clean Fusion profile is created by Fusion in the predefined folder. The default locaion is set to `~/.openpype/hosts/fusion/prefs`, to better comply with the other os platforms. After creating the default profile, some modifications are applied: +- forced Python3 +- forced English interface +- setup Openpype specific path maps.If the `copy_prefs` checkbox is toggled, a copy of existing Fusion profile folder will be placed in the mentioned location. Then they are altered the same way as described above. The operation is run only once, on the first launch, unless the `force_sync [Resync profile on each launch]` is toggled.English interface is forced because the `FUSION16_PROFILE_DIR` environment variable is not read otherwise (seems to be a Fusion bug). + + +___ + +
    + + +
    +Fusion publish existing frames #4611 + +This PR adds the function to publish existing frames instead of having to re-render all of them for each new publish.I have split the render_locally plugin so the review-part is its own plugin now.I also change the saver-creator-plugin's label from Saver to Render (saver) as I intend to add a Prerender creator like in Nuke. + + +___ + +
    + + +
    +Clockify: refresh and fix the integration #4607 + +Due to recent API changes, Clockify requires `user_id` to operate with the timers. I updated this part and currently it is a WIP for making it fully functional. Most functions, such as start and stop timer, and projects sync are currently working. For the rate limiting task new dependency is added: https://pypi.org/project/ratelimiter/ + + +___ + +
    + + +
    +Publisher: Windows reduce command window pop-ups during Publishing #4672 + +Reduce the command line pop-ups that show on Windows during publishing. + + +___ + +
    + + +
    +CelAction: conditional workfile parameters from settings #4677 + +Since some productions were requesting excluding some workfile parameters from publishing submission, we needed to move them to settings so those could be altered per project. + + +___ + +
    + + +
    +Fix name and docstring for Create Workdir Extra Folders prelaunch hook #4683 + +Fix class name and docstring for Create Workdir Extra Folders prelaunch hookThe class name and docstring were originally copied from another plug-in and didn't match the plug-in logic.This also fixes potentially seeing this twice in your logs. Before:After:Where it was actually running both this prelaunch hook and the actual `AddLastWorkfileToLaunchArgs` plugin. + + +___ + +
    + +### **🐛 Bug fixes** + + +
    +Maya: Fix getting non-active model panel. #2968 + +When capturing multiple cameras with image planes that have file sequences playing, only the active (first) camera will play through the file sequence. + + +___ + +
    + + +
    +Maya: Fix broken review publishing. #4549 + +Resolves #4547 + + +___ + +
    + + +
    +Maya: Avoid error on right click in Loader if `mtoa` is not loaded #4616 + +Fix an error on right clicking in the Loader when `mtoa` is not a loaded plug-in.Additionally if `mtoa` isn't loaded the loader will now load the plug-in before trying to create the arnold standin. + + +___ + +
    + + +
    +Maya: Fix extract look colorspace detection #4618 + +Fix the logic which guesses the colorspace using `arnold` python library. +- Previously it'd error if `mtoa` was not available on path so it still required `mtoa` to be available. +- The guessing colorspace logic doesn't actually require `mtoa` to be loaded, but just the `arnold` python library to be available. This changes the logic so it doesn't require the `mtoa` plugin to get loaded to guess the colorspace. +- The if/else branch was likely not doing what was intended `cmds.loadPlugin("mtoa", quiet=True)` returns None if the plug-in was already loaded. So this would only ever be true if it ends up loading the `mtoa` plugin the first time. +```python +# Tested in Maya 2022.1 +print(cmds.loadPlugin("mtoa", quiet=True)) +# ['mtoa'] +print(cmds.loadPlugin("mtoa", quiet=True)) +# None +``` + + +___ + +
    + + +
    +Maya: Maya Playblast Options overrides - OP-3847 #4634 + +When publishing a review in Maya, the extractor would fail due to wrong (long) panel name. + + +___ + +
    + + +
    +Bugfix/op 2834 fix extract playblast #4701 + +Paragraphs contain detailed information on the changes made to the product or service, providing an in-depth description of the updates and enhancements. They can be used to explain the reasoning behind the changes, or to highlight the importance of the new features. Paragraphs can often include links to further information or support documentation. + + +___ + +
    + + +
    +Bugfix/op 2834 fix extract playblast #4704 + +Paragraphs contain detailed information on the changes made to the product or service, providing an in-depth description of the updates and enhancements. They can be used to explain the reasoning behind the changes, or to highlight the importance of the new features. Paragraphs can often include links to further information or support documentation. + + +___ + +
    + + +
    +Maya: bug fix for passing zoom settings if review is attached to subset #4716 + +Fix for attaching review to subset with pan/zoom option. + + +___ + +
    + + +
    +Fixed a bug where a QThread in the splash screen could be destroyed before finishing execution #4647 + +This should fix the occasional behavior of the QThread being destroyed before even its worker returns from the `run()` function.After quiting, it should wait for the QThread object to properly close itself. + + +___ + +
    + + +
    +Max: fix the bug of removing an instance #4617 + +fix the bug of removing an instance in 3dsMax + + +___ + +
    + + +
    +bugfix for 3dsmax publishing error #4637 + +fix the bug of failing publishing job in 3dsMax + + +___ + +
    + + +
    +3dsmax: opening last workfile #4644 + +Supports opening last saved workfile in 3dsmax host. + + +___ + +
    + + +
    +Global | Nuke: fixing farm publishing workflow #4623 + +After Nuke had adopted new publisher with new creators new issues were introduced. Those issues were addressed with this PR. Those are for example broken reviewable video files publishing if published via farm. Also fixed local publishing. + + +___ + +
    + + +
    +Nuke: Nukenodes family instance without frame range #4669 + +No need to add frame range data into `nukenodes` (backdrop) family publishes - since those are timeless. + + +___ + +
    + + +
    +TVPaint: Optional Validation plugins can be de/activated by user #4674 + +Added `OptionalPyblishPluginMixin` to TVpaint plugins that can be optional. + + +___ + +
    + + +
    +Hiero: Creator with correct workfile numeric padding input #4666 + +Creator was showing 99 in workfile input for long time, even if users set default value to 1001 in studio settings. This has been fixed now. + + +___ + +
    + + +
    +Hiero: correct container colors if UpToDate #4708 + +Colors on loaded containers are now correctly identifying real state of version. `Red` for out of date and `green` for up to date. + + +___ + +
    + + +
    +Maya: tile assembly fail in draft - OP-4820 #4416 + +Tile assembly in Deadline was broken. + +Initial bug report revealed other areas of the tile assembly that needed fixing. + + +___ + +
    + + +
    +Scene inventory: Fix code errors when "not found" entries are found #4594 + +Whenever a "NOT FOUND" entry is present a lot of errors happened in the Scene Inventory: +- It started spamming a lot of errors for the VersionDelegate since it had no numeric version (no version at all).Error reported on Discord: +```python +Traceback (most recent call last): + File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\utils\delegates.py", line 65, in paint + text = self.displayText( + File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\utils\delegates.py", line 33, in displayText + assert isinstance(value, numbers.Integral), ( +AssertionError: Version is not integer. "None" +``` +- Right click menu would error on NOT FOUND entries, and thus not show. With this PR it will now _disregard_ not found items for "Set version" and "Remove" but still allow actions.This PR resolves those. + + +___ + +
    + + +
    +Ftrack: Ftrack additional families filtering #4633 + +Ftrack family collector makes sure the subset family is also in instance families for additional families filtering. + + +___ + +
    + + +
    +General: Use right validation for ffmpeg executable #4640 + +Use ffmpeg exec validation for ffmpeg executables instead of oiio exec validation. The validation is used as last possible source of ffmpeg from `PATH` environment variables, which is an edge case but can cause issues. + + +___ + +
    + + +
    +Global: add tags field to thumbnail representation #4660 + +Thumbnail representation might be missing tags field. + + +___ + +
    + + +
    +Kitsu: Slightly less strict with instance data #4678 + +- Allow to take task name from context if asset doesn't have any. Fixes an issue with Photoshop's review instance not having `task` in data. +- Allow to match "review" against both `instance.data["family"]` and `instance.data["families"]` because some instances don't have the primary family in families, e.g. in Photoshop and TVPaint. +- Do not error on Integrate Kitsu Review whenever for whatever reason Integrate Kitsu Note did not created a comment but just log the message that it was unable to connect a review. + + +___ + +
    + + +
    +Refactor _capture #4702 + +Paragraphs contain detailed information on the changes made to the product or service, providing an in-depth description of the updates and enhancements. They can be used to explain the reasoning behind the changes, or to highlight the importance of the new features. Paragraphs can often include links to further information or support documentation. + + +___ + +
    + +### **🔀 Refactored code** + + +
    +Look Assigner: Move Look Assigner tool since it's Maya only #4604 + +Fix #4357: Move Look Assigner tool to maya since it's Maya only + + +___ + +
    + + +
    +Maya: Remove unused functions from Extract Look #4671 + +Remove unused functions from Maya Extract Look plug-in + + +___ + +
    + + +
    +Extract Review code refactor #3930 + +Trying to reduce complexity of Extract Review plug-in +- Re-use profile filtering from lib +- Remove "combination families" additional filtering which supposedly was from OP v2 +- Simplify 'formatting' for filling gaps +- Use `legacy_io.Session` over `os.environ` + + +___ + +
    + + +
    +Update tests and documentation for `ColormanagedPyblishPluginMixin` #4612 + +Refactor `ExtractorColormanaged` to `ColormanagedPyblishPluginMixin` in tests and documentation. + + +___ + +
    + +### **📃 Documentation** + + +
    +Docs/add architecture document #4344 + +Add `ARCHITECTURE.md` document. + +his document attemps to give a quick overview of the project to help onboarding, it's not an extensive documentation but more of a elevator pitch one-line descriptions of files/directories and what the attempt to do. + + +___ + +
    + + +
    +Docs: Fix some minor grammar/typos #4680 + +Typo/grammar fixes in documentation. + + +___ + +
    + +### **Merged pull requests** + + +
    +Maya: Implement image file node loader #4313 + +Implements a loader for loading texture image into a `file` node in Maya. + +Similar to Maya's hypershade creation of textures on load you have the option to choose for three modes of creating: +- Texture +- Projection +- StencilThese should match what Maya generates if you create those in Maya. +- [x] Load and manage file nodes +- [x] Apply color spaces after #4195 +- [x] Support for _either_ UDIM or image sequence - currently it seems to always load sequences as UDIM automatically. +- [ ] Add support for animation sequences of UDIM textures using the `..exr` path format? + + +___ + +
    + + +
    +Maya Look Assigner: Don't rely on containers for get all assets #4600 + +This resolves #4044 by not actually relying on containers in the scene but instead just rely on finding nodes with `cbId` attributes. As such, imported nodes would also be found and a shader can be assigned (similar to when using get from selection).**Please take into consideration the potential downsides below**Potential downsides would be: +- IF an already loaded look has any dagNodes, say a 3D Projection node - then that will also show up as a loaded asset where previously nodes from loaded looks were ignored. +- If any dag nodes were created locally - they would have gotten `cbId` attributes on scene save and thus the current asset would almost always show? + + +___ + +
    + + +
    +Maya: Unify menu labels for "Set Frame Range" and "Set Resolution" #4605 + +Fix #4109: Unify menu labels for "Set Frame Range" and "Set Resolution"This also tweaks it in Houdini from Reset Frame Range to Set Frame Range. + + +___ + +
    + + +
    +3dsmax: make sure that startup script executes #4695 + +Fixing reliability of OpenPype startup in 3dsmax. + + +___ + +
    + + +
    +Resolve missing OPENPYPE_MONGO in deadline global job preload #4484 + +In the GlobalJobPreLoad plugin, we propose to replace the SpawnProcess by a sub-process and to pass the environment variables in the parameters, since the SpawnProcess under Centos Linux does not pass the environment variables. + +In the GlobalJobPreLoad plugin, the Deadline SpawnProcess is used to start the OpenPype process. The problem is that the SpawnProcess does not pass environment variables, including OPENPYPE_MONGO, to the process when it is under Centos7 linux, and the process gets stuck. We propose to replace it by a subprocess and to pass the variable in the parameters. + + +___ + +
    + + +
    +Maya: Arnold don't reset maya timeline frame range on render creation (or setting render settings) #4603 + +Fix #4429: Do not reset fps or playback timeline on applying or creating render settings + + +___ + +
    + + +
    +Update artist_hosts_maya_arnold.md #4626 + +Correct Arnold docs. +___ + +
    + + +
    +General: Filter available applications #4667 + +Added option to filter applications that don't have valid executable available in settings in launcher and ftrack actions. This option can be disabled in new settings category `Applications`. The filtering is by default disabled. + + +___ + +
    + + + + [Full Changelog](https://github.com/ynput/OpenPype/compare/3.15.1...3.15.2) ### **🆕 New features** diff --git a/openpype/version.py b/openpype/version.py index bc5ea7fe7c..b20b133914 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.3-nightly.4" +__version__ = "3.15.3" diff --git a/pyproject.toml b/pyproject.toml index 02370a4f10..42ce5aa32c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.15.2" # OpenPype +version = "3.15.3" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 85196b68f4273518d62f83473df228487e3082d2 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 27 Mar 2023 17:06:13 +0100 Subject: [PATCH 188/228] Add maya specific burnin profile. --- .../defaults/project_settings/global.json | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index aad17d54da..eb74078cf0 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -249,6 +249,29 @@ } } } + }, + { + "families": [], + "hosts": [ + "maya" + ], + "task_types": [], + "task_names": [], + "subsets": [], + "burnins": { + "maya_burnin": { + "TOP_LEFT": "{yy}-{mm}-{dd}", + "TOP_CENTERED": "{focalLength:.2f} mm", + "TOP_RIGHT": "{anatomy[version]}", + "BOTTOM_LEFT": "{username}", + "BOTTOM_CENTERED": "{asset}", + "BOTTOM_RIGHT": "{frame_start}-{current_frame}-{frame_end}", + "filter": { + "families": [], + "tags": [] + } + } + } } ] }, From c758ec151fa93c8d43e3a0adc95d8cc6326edd7a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 28 Mar 2023 12:57:44 +0200 Subject: [PATCH 189/228] fix action trigger to be only changes_requested --- .github/workflows/project_actions.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/project_actions.yml b/.github/workflows/project_actions.yml index ca94f3ae77..26bc2b8a1f 100644 --- a/.github/workflows/project_actions.yml +++ b/.github/workflows/project_actions.yml @@ -10,7 +10,7 @@ jobs: pr_review_requested: name: pr_review_requested runs-on: ubuntu-latest - if: github.event_name == 'pull_request' && github.event.action == 'review_requested' + if: github.event_name == 'pull_request_review' && github.event.review.state == 'changes_requested' steps: - name: Move PR to 'Change Requested' uses: leonsteinhaeuser/project-beta-automations@v2.1.0 From 51cf5d2436932988fc1c2218ed1c1aec537b065a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 28 Mar 2023 15:16:57 +0200 Subject: [PATCH 190/228] updating changelog.md --- CHANGELOG.md | 602 ++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 430 insertions(+), 172 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78ea53f3d0..4e22b783c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,26 @@ # Changelog +## [3.15.3](https://github.com/ynput/OpenPype/tree/3.15.3) + [Full Changelog](https://github.com/ynput/OpenPype/compare/3.15.2...3.15.3) ### **🆕 New features** +
    +Blender: Extract Review #3616 + +Added Review to Blender. + +This implementation is based on #3508 but made compatible for the current implementation of OpenPype for Blender. + + +___ + +
    + +
    Data Exchanges: Point Cloud for 3dsMax #4532 @@ -23,30 +38,6 @@ ___
    -
    -MaxScene Family #4615 - -Introduction of the Max Scene Family - - -___ - -
    - - -
    -Blender: Extract Review #3616 - -Added Review to Blender. - -This implementation is based on #3508 but made compatible for the current implementation of OpenPype for Blender. - - -___ - -
    - -
    Global: persistent staging directory for renders #4583 @@ -55,6 +46,28 @@ ___ With this feature, users can specify a transient data folder path based on presets, which can be used during the creation and publishing stages. In some cases, these DCCs automatically add a rendering path during the creation stage, which is then used in publishing.One of the key advantages of this feature is that it allows users to take advantage of faster storages for rendering, which can help improve workflow efficiency. Additionally, this feature allows users to keep their rendered data persistent, and use their own infrastructure for regular cleaning.However, it should be noted that some productions may want to use this feature without persistency. Furthermore, there may be a need for retargeting the rendering folder to faster storages, which is also not supported at the moment.It is studio responsibility to clean up obsolete folders with data.Location of the folder is configured in `project_anatomy/templates/others`. ('transient' key is expected, with 'folder' key, could be more templates)Which family/task type/subset is applicable is configured in:`project_settings/global/tools/publish/transient_dir_profiles` +___ + +
    + + +
    +Kitsu custom comment template #4599 + +Kitsu allows to write markdown in its comment field. This can be something very powerful to deliver dynamic comments with the help the data from the instance.This feature is defaults to off so the admin have to manually set up the comment field the way they want.I have added a basic example on how the comment can look like as the comment-fields default value.To this I want to add some documentation also but that's on its way when the code itself looks good for the reviewers. + + +___ + +
    + + +
    +MaxScene Family #4615 + +Introduction of the Max Scene Family + + ___
    @@ -84,6 +97,22 @@ ___ +
    +Copy existing or generate new Fusion profile on prelaunch #4572 + +Fusion preferences will be copied to the predefined `~/.openpype/hosts/fusion/prefs` folder (or any other folder set in system settings) on launch. + +The idea is to create a copy of existing Fusion profile, adding an OpenPype menu to the Fusion instance.By default the copy setting is turned off, so no file copying is performed. Instead the clean Fusion profile is created by Fusion in the predefined folder. The default locaion is set to `~/.openpype/hosts/fusion/prefs`, to better comply with the other os platforms. After creating the default profile, some modifications are applied: +- forced Python3 +- forced English interface +- setup Openpype specific path maps.If the `copy_prefs` checkbox is toggled, a copy of existing Fusion profile folder will be placed in the mentioned location. Then they are altered the same way as described above. The operation is run only once, on the first launch, unless the `force_sync [Resync profile on each launch]` is toggled.English interface is forced because the `FUSION16_PROFILE_DIR` environment variable is not read otherwise (seems to be a Fusion bug). + + +___ + +
    + +
    Houdini: Create button open new publisher's "create" tab #4601 @@ -95,6 +124,28 @@ ___
    +
    +Clockify: refresh and fix the integration #4607 + +Due to recent API changes, Clockify requires `user_id` to operate with the timers. I updated this part and currently it is a WIP for making it fully functional. Most functions, such as start and stop timer, and projects sync are currently working. For the rate limiting task new dependency is added: https://pypi.org/project/ratelimiter/ + + +___ + +
    + + +
    +Fusion publish existing frames #4611 + +This PR adds the function to publish existing frames instead of having to re-render all of them for each new publish.I have split the render_locally plugin so the review-part is its own plugin now.I also change the saver-creator-plugin's label from Saver to Render (saver) as I intend to add a Prerender creator like in Nuke. + + +___ + +
    + +
    Resolution settings referenced from DB record for 3dsMax #4652 @@ -107,6 +158,17 @@ ___
    +
    +3dsmax: render instance settings in Publish tab #4658 + +Allows user preset the pools, group and use_published settings in Render Creator in the Max Hosts.User can set the settings before or after creating instance in the new publisher + + +___ + +
    + +
    scene length setting referenced from DB record for 3dsMax #4665 @@ -118,55 +180,6 @@ ___
    -
    -increment workfile version 3dsmax #4685 - -increment workfile version in 3dsmax as if in blender and maya hosts. - - -___ - -
    - - -
    -Copy existing or generate new Fusion profile on prelaunch #4572 - -Fusion preferences will be copied to the predefined `~/.openpype/hosts/fusion/prefs` folder (or any other folder set in system settings) on launch. - -The idea is to create a copy of existing Fusion profile, adding an OpenPype menu to the Fusion instance.By default the copy setting is turned off, so no file copying is performed. Instead the clean Fusion profile is created by Fusion in the predefined folder. The default locaion is set to `~/.openpype/hosts/fusion/prefs`, to better comply with the other os platforms. After creating the default profile, some modifications are applied: -- forced Python3 -- forced English interface -- setup Openpype specific path maps.If the `copy_prefs` checkbox is toggled, a copy of existing Fusion profile folder will be placed in the mentioned location. Then they are altered the same way as described above. The operation is run only once, on the first launch, unless the `force_sync [Resync profile on each launch]` is toggled.English interface is forced because the `FUSION16_PROFILE_DIR` environment variable is not read otherwise (seems to be a Fusion bug). - - -___ - -
    - - -
    -Fusion publish existing frames #4611 - -This PR adds the function to publish existing frames instead of having to re-render all of them for each new publish.I have split the render_locally plugin so the review-part is its own plugin now.I also change the saver-creator-plugin's label from Saver to Render (saver) as I intend to add a Prerender creator like in Nuke. - - -___ - -
    - - -
    -Clockify: refresh and fix the integration #4607 - -Due to recent API changes, Clockify requires `user_id` to operate with the timers. I updated this part and currently it is a WIP for making it fully functional. Most functions, such as start and stop timer, and projects sync are currently working. For the rate limiting task new dependency is added: https://pypi.org/project/ratelimiter/ - - -___ - -
    - -
    Publisher: Windows reduce command window pop-ups during Publishing #4672 @@ -178,6 +191,17 @@ ___
    +
    +Publisher: Explicit save #4676 + +Publisher have explicit button to save changes, so reset can happen without saving any changes. Save still happens automatically when publishing is started or on publisher window close. But a popup is shown if context of host has changed. Important context was enhanced by workfile path (if host integration supports it) so workfile changes are captured too. In that case a dialog with confirmation is shown to user. All callbacks that may require save of context were moved to main window to be able handle dialog show at one place. Save changes now returns success so the rest of logic is skipped -> publishing won't start, when save of instances fails.Save and reset buttons have shortcuts (Ctrl + s and Ctrls + r). + + +___ + +
    + +
    CelAction: conditional workfile parameters from settings #4677 @@ -189,12 +213,45 @@ ___
    +
    +Improve logging of used app + tool envs on application launch #4682 + +Improve logging of what apps + tool environments got loaded for an application launch. + + +___ + +
    + +
    Fix name and docstring for Create Workdir Extra Folders prelaunch hook #4683 Fix class name and docstring for Create Workdir Extra Folders prelaunch hookThe class name and docstring were originally copied from another plug-in and didn't match the plug-in logic.This also fixes potentially seeing this twice in your logs. Before:After:Where it was actually running both this prelaunch hook and the actual `AddLastWorkfileToLaunchArgs` plugin. +___ + +
    + + +
    +Application launch context: Include app group name in logger #4684 + +Clarify in logs better what app group the ApplicationLaunchContext belongs to and what application is being launched.Before:After: + + +___ + +
    + + +
    +increment workfile version 3dsmax #4685 + +increment workfile version in 3dsmax as if in blender and maya hosts. + + ___
    @@ -301,9 +358,64 @@ ___
    -Fixed a bug where a QThread in the splash screen could be destroyed before finishing execution #4647 +Maya: tile assembly fail in draft - OP-4820 #4416 -This should fix the occasional behavior of the QThread being destroyed before even its worker returns from the `run()` function.After quiting, it should wait for the QThread object to properly close itself. +Tile assembly in Deadline was broken. + +Initial bug report revealed other areas of the tile assembly that needed fixing. + + +___ + +
    + + +
    +Maya: Yeti Validate Rig Input - OP-3454 #4554 + +Fix Yeti Validate Rig Input + +Existing workflow was broken due to this #3297. + + +___ + +
    + + +
    +Scene inventory: Fix code errors when "not found" entries are found #4594 + +Whenever a "NOT FOUND" entry is present a lot of errors happened in the Scene Inventory: +- It started spamming a lot of errors for the VersionDelegate since it had no numeric version (no version at all).Error reported on Discord: +```python +Traceback (most recent call last): + File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\utils\delegates.py", line 65, in paint + text = self.displayText( + File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\utils\delegates.py", line 33, in displayText + assert isinstance(value, numbers.Integral), ( +AssertionError: Version is not integer. "None" +``` +- Right click menu would error on NOT FOUND entries, and thus not show. With this PR it will now _disregard_ not found items for "Set version" and "Remove" but still allow actions.This PR resolves those. + + +___ + +
    + + +
    +Kitsu: Sync OP with zou, make sure value-data is int or float #4596 + +Currently the data zou pulls is a string and not a value causing some bugs in the pipe where a value is expected (like `Set frame range` in Fusion). + + + +This PR makes sure each value is set with int() or float() so these bugs can't happen later on. + + + +_(A request to cgwire has also bin sent to allow force values only for some metadata columns, but currently the user can enter what ever they want in there)_ ___ @@ -322,6 +434,39 @@ ___
    +
    +Global | Nuke: fixing farm publishing workflow #4623 + +After Nuke had adopted new publisher with new creators new issues were introduced. Those issues were addressed with this PR. Those are for example broken reviewable video files publishing if published via farm. Also fixed local publishing. + + +___ + +
    + + +
    +Ftrack: Ftrack additional families filtering #4633 + +Ftrack family collector makes sure the subset family is also in instance families for additional families filtering. + + +___ + +
    + + +
    +Ftrack: Hierarchical <> Non-Hierarchical attributes sync fix #4635 + +Sync between hierarchical and non-hierarchical attributes should be fixed and work as expected. Action should sync the values as expected and event handler should do it too and only on newly created entities. + + +___ + +
    + +
    bugfix for 3dsmax publishing error #4637 @@ -333,6 +478,17 @@ ___
    +
    +General: Use right validation for ffmpeg executable #4640 + +Use ffmpeg exec validation for ffmpeg executables instead of oiio exec validation. The validation is used as last possible source of ffmpeg from `PATH` environment variables, which is an edge case but can cause issues. + + +___ + +
    + +
    3dsmax: opening last workfile #4644 @@ -345,9 +501,53 @@ ___
    -Global | Nuke: fixing farm publishing workflow #4623 +Fixed a bug where a QThread in the splash screen could be destroyed before finishing execution #4647 -After Nuke had adopted new publisher with new creators new issues were introduced. Those issues were addressed with this PR. Those are for example broken reviewable video files publishing if published via farm. Also fixed local publishing. +This should fix the occasional behavior of the QThread being destroyed before even its worker returns from the `run()` function.After quiting, it should wait for the QThread object to properly close itself. + + +___ + +
    + + +
    +General: Use right plugin class for Collect Comment #4653 + +Collect Comment plugin is instance plugin so should inherit from `InstancePlugin` instead of `ContextPlugin`. + + +___ + +
    + + +
    +Global: add tags field to thumbnail representation #4660 + +Thumbnail representation might be missing tags field. + + +___ + +
    + + +
    +Integrator: Enforce unique destination transfers, disallow overwrites in queued transfers #4662 + +Fix #4656 by enforcing unique destination transfers in the Integrator. It's now disallowed to a destination in the file transaction queue with a new source path during the publish. + + +___ + +
    + + +
    +Hiero: Creator with correct workfile numeric padding input #4666 + +Creator was showing 99 in workfile input for long time, even if users set default value to 1001 in studio settings. This has been fixed now. ___ @@ -377,95 +577,6 @@ ___
    -
    -Hiero: Creator with correct workfile numeric padding input #4666 - -Creator was showing 99 in workfile input for long time, even if users set default value to 1001 in studio settings. This has been fixed now. - - -___ - -
    - - -
    -Hiero: correct container colors if UpToDate #4708 - -Colors on loaded containers are now correctly identifying real state of version. `Red` for out of date and `green` for up to date. - - -___ - -
    - - -
    -Maya: tile assembly fail in draft - OP-4820 #4416 - -Tile assembly in Deadline was broken. - -Initial bug report revealed other areas of the tile assembly that needed fixing. - - -___ - -
    - - -
    -Scene inventory: Fix code errors when "not found" entries are found #4594 - -Whenever a "NOT FOUND" entry is present a lot of errors happened in the Scene Inventory: -- It started spamming a lot of errors for the VersionDelegate since it had no numeric version (no version at all).Error reported on Discord: -```python -Traceback (most recent call last): - File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\utils\delegates.py", line 65, in paint - text = self.displayText( - File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\utils\delegates.py", line 33, in displayText - assert isinstance(value, numbers.Integral), ( -AssertionError: Version is not integer. "None" -``` -- Right click menu would error on NOT FOUND entries, and thus not show. With this PR it will now _disregard_ not found items for "Set version" and "Remove" but still allow actions.This PR resolves those. - - -___ - -
    - - -
    -Ftrack: Ftrack additional families filtering #4633 - -Ftrack family collector makes sure the subset family is also in instance families for additional families filtering. - - -___ - -
    - - -
    -General: Use right validation for ffmpeg executable #4640 - -Use ffmpeg exec validation for ffmpeg executables instead of oiio exec validation. The validation is used as last possible source of ffmpeg from `PATH` environment variables, which is an edge case but can cause issues. - - -___ - -
    - - -
    -Global: add tags field to thumbnail representation #4660 - -Thumbnail representation might be missing tags field. - - -___ - -
    - -
    Kitsu: Slightly less strict with instance data #4678 @@ -479,12 +590,34 @@ ___
    +
    +Publisher: Fix reset shortcut sequence #4694 + +Fix bug created in https://github.com/ynput/OpenPype/pull/4676 where key sequence is checked using unsupported method. The check was changed to convert event into `QKeySequence` object which can be compared to prepared sequence. + + +___ + +
    + +
    Refactor _capture #4702 Paragraphs contain detailed information on the changes made to the product or service, providing an in-depth description of the updates and enhancements. They can be used to explain the reasoning behind the changes, or to highlight the importance of the new features. Paragraphs can often include links to further information or support documentation. +___ + +
    + + +
    +Hiero: correct container colors if UpToDate #4708 + +Colors on loaded containers are now correctly identifying real state of version. `Red` for out of date and `green` for up to date. + + ___
    @@ -529,12 +662,34 @@ ___
    +
    +Maya: Replace last usages of Qt module #4610 + +Replace last usage of `Qt` module with `qtpy`. This change is needed for `PySide6` support. All changes happened in Maya loader plugins. + + +___ + +
    + +
    Update tests and documentation for `ColormanagedPyblishPluginMixin` #4612 Refactor `ExtractorColormanaged` to `ColormanagedPyblishPluginMixin` in tests and documentation. +___ + +
    + + +
    +Improve logging of used app + tool envs on application launch (minor tweak) #4686 + +Use `app.full_name` for change done in #4682 + + ___
    @@ -555,6 +710,17 @@ ___ +
    +Documentation: Tweak grammar and fix some typos #4613 + +This resolves some grammar and typos in the documentation.Also fixes the extension of some images in after effects docs which used uppercase extension even though files were lowercase extension. + + +___ + +
    + +
    Docs: Fix some minor grammar/typos #4680 @@ -613,9 +779,11 @@ ___
    -3dsmax: make sure that startup script executes #4695 +Resolve missing OPENPYPE_MONGO in deadline global job preload #4484 -Fixing reliability of OpenPype startup in 3dsmax. +In the GlobalJobPreLoad plugin, we propose to replace the SpawnProcess by a sub-process and to pass the environment variables in the parameters, since the SpawnProcess under Centos Linux does not pass the environment variables. + +In the GlobalJobPreLoad plugin, the Deadline SpawnProcess is used to start the OpenPype process. The problem is that the SpawnProcess does not pass environment variables, including OPENPYPE_MONGO, to the process when it is under Centos7 linux, and the process gets stuck. We propose to replace it by a subprocess and to pass the variable in the parameters. ___ @@ -624,11 +792,9 @@ ___
    -Resolve missing OPENPYPE_MONGO in deadline global job preload #4484 +Tests: Added setup_only to tests #4591 -In the GlobalJobPreLoad plugin, we propose to replace the SpawnProcess by a sub-process and to pass the environment variables in the parameters, since the SpawnProcess under Centos Linux does not pass the environment variables. - -In the GlobalJobPreLoad plugin, the Deadline SpawnProcess is used to start the OpenPype process. The problem is that the SpawnProcess does not pass environment variables, including OPENPYPE_MONGO, to the process when it is under Centos7 linux, and the process gets stuck. We propose to replace it by a subprocess and to pass the variable in the parameters. +Allows to download test zip, unzip and restore DB in preparation for new test. ___ @@ -647,6 +813,64 @@ ___
    +
    +Bump @sideway/formula from 3.0.0 to 3.0.1 in /website #4609 + +Bumps [@sideway/formula](https://github.com/sideway/formula) from 3.0.0 to 3.0.1. +
    +Commits + +
    +
    +Maintainer changes +

    This version was pushed to npm by marsup, a new releaser for @​sideway/formula since your current version.

    +
    +
    + + +[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@sideway/formula&package-manager=npm_and_yarn&previous-version=3.0.0&new-version=3.0.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) + +Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. + +[//]: # (dependabot-automerge-start) +[//]: # (dependabot-automerge-end) + +--- + +
    +Dependabot commands and options +
    + +You can trigger Dependabot actions by commenting on this PR: +- `@dependabot rebase` will rebase this PR +- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it +- `@dependabot merge` will merge this PR after your CI passes on it +- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it +- `@dependabot cancel merge` will cancel a previously requested merge and block automerging +- `@dependabot reopen` will reopen this PR if it is closed +- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually +- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) +- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) +- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) +- `@dependabot use these labels` will set the current labels as the default for future PRs for this repo and language +- `@dependabot use these reviewers` will set the current reviewers as the default for future PRs for this repo and language +- `@dependabot use these assignees` will set the current assignees as the default for future PRs for this repo and language +- `@dependabot use this milestone` will set the current milestone as the default for future PRs for this repo and language + +You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/ynput/OpenPype/network/alerts). + +
    +___ + +
    + +
    Update artist_hosts_maya_arnold.md #4626 @@ -656,6 +880,17 @@ ___
    +
    +Maya: Add "Include Parent Hierarchy" option in animation creator plugin #4645 + +Add an option in Project Settings > Maya > Creator Plugins > Create Animation to include (or not) parent hierarchy. This is to avoid artists to check manually the option for all create animation. + + +___ + +
    + +
    General: Filter available applications #4667 @@ -667,7 +902,30 @@ ___
    +
    +3dsmax: make sure that startup script executes #4695 +Fixing reliability of OpenPype startup in 3dsmax. + + +___ + +
    + + +
    +Project Manager: Change minimum frame start/end to '0' #4719 + +Project manager can have frame start/end set to `0`. + + +___ + +
    + + + +## [3.15.2](https://github.com/ynput/OpenPype/tree/3.15.2) [Full Changelog](https://github.com/ynput/OpenPype/compare/3.15.1...3.15.2) From ba1e033815f25d9d9f30a110b3a0cdfc81f3ae36 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 29 Mar 2023 03:26:05 +0000 Subject: [PATCH 191/228] [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 b20b133914..4d6ee5590e 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.3" +__version__ = "3.15.4-nightly.1" From 7264ce3aafd51c8fbd81dd1803a9242c161ad75b Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Wed, 29 Mar 2023 09:17:20 +0100 Subject: [PATCH 192/228] Update openpype/lib/execute.py --- openpype/lib/execute.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/lib/execute.py b/openpype/lib/execute.py index 5ba62f177f..eba75389f1 100644 --- a/openpype/lib/execute.py +++ b/openpype/lib/execute.py @@ -199,7 +199,7 @@ def run_openpype_process(*args, **kwargs): # - fill more if you find more env = clean_envs_for_openpype_process(os.environ) - # Add OpenPype version if we are running from build. + # Only keep OpenPype version if we are running from build. if not is_running_from_build(): env.pop("OPENPYPE_VERSION", None) From 421048083164e549a69bdc16e248b33d7cc0c71f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 29 Mar 2023 12:51:07 +0200 Subject: [PATCH 193/228] Refactor ExtractLook maketx argument in settings to more structured arguments/parameters --- .../maya/plugins/publish/extract_look.py | 20 +++++++++---------- .../defaults/project_settings/maya.json | 2 +- .../schemas/schema_maya_publish.json | 17 ++++++++++++++-- 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 1e339542d7..93054e5fbb 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -227,21 +227,21 @@ class MakeTX(TextureProcessor): def apply_settings(self, system_settings, project_settings): # Allow extra maketx arguments from project settings - extra_args_dict = ( + args_settings = ( project_settings["maya"]["publish"] - .get("ExtractLook", {}).get("maketx_arguments", {}) + .get("ExtractLook", {}).get("maketx_arguments", []) ) extra_args = [] - for flag, value in extra_args_dict.items(): - if not flag: - self.log.debug("Ignoring empty flag from `maketx_arguments` " - "setting..") + for arg_data in args_settings: + argument = arg_data["argument"] + parameters = arg_data["parameters"] + if not argument: + self.log.debug("Ignoring empty parameter from " + "`maketx_arguments` setting..") continue - extra_args.append(flag) - if value.strip(): - # There might be flags without values like --opaque-detect - extra_args.append(value) + extra_args.append(argument) + extra_args.extend(parameters) self.extra_args = extra_args diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 26dd66770f..8f5a3c75ab 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -928,7 +928,7 @@ "ogsfx_path": "/maya2glTF/PBR/shaders/glTF_PBR.ogsfx" }, "ExtractLook": { - "maketx_arguments": {} + "maketx_arguments": [] } }, "load": { diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json index da12dde6b2..7ced375cb5 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json @@ -1004,11 +1004,24 @@ "label": "Extract Look", "children": [ { + "type": "list", "key": "maketx_arguments", "label": "Extra arguments for maketx command line", - "type": "dict-modifiable", "object_type": { - "type": "text" + "type": "dict", + "children": [ + { + "key": "argument", + "label": "Argument", + "type": "text" + }, + { + "key": "parameters", + "label": "Parameters", + "type": "list", + "object_type": "text" + } + ] } } ] From 3fa284f16ca8ac21fef535467289e1ca3e6607ac Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 29 Mar 2023 14:09:52 +0200 Subject: [PATCH 194/228] adding size labeling to project workflow --- .github/workflows/project_actions.yml | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/.github/workflows/project_actions.yml b/.github/workflows/project_actions.yml index 26bc2b8a1f..dc68a9dc0e 100644 --- a/.github/workflows/project_actions.yml +++ b/.github/workflows/project_actions.yml @@ -2,7 +2,7 @@ name: project-actions on: pull_request: - types: [review_requested] + types: [opened, synchronize, assigned, review_requested] pull_request_review: types: [submitted] @@ -20,3 +20,26 @@ jobs: project_id: 11 resource_node_id: ${{ github.event.pull_request.node_id }} status_value: Change Requested + size-label: + name: pr_size_label + runs-on: ubuntu-latest + if: | + ${{(github.event_name == 'pull_request' && github.event.action == 'synchronize') + || (github.event_name == 'pull_request' && github.event.action == 'assigned')}} + + steps: + - name: Add size label + uses: "pascalgn/size-label-action@v0.4.3" + env: + GITHUB_TOKEN: "${{ secrets.YNPUT_BOT_TOKEN }}" + IGNORED: ".gitignore\n*.md\n*.json" + with: + sizes: > + { + "0": "XS", + "100": "S", + "500": "M", + "1000": "L", + "1500": "XL", + "2500": "XXL" + } \ No newline at end of file From 1e6b57209fbddefda6619b78d9d1e8d848e34c91 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 29 Mar 2023 14:47:33 +0200 Subject: [PATCH 195/228] Adding labelling to project action --- .github/pr-branch-labeler.yml | 15 +++++++++++++++ .github/pr-glob-labeler.yml | 7 +++++++ .github/workflows/project_actions.yml | 24 +++++++++++++++++++++++- 3 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 .github/pr-branch-labeler.yml create mode 100644 .github/pr-glob-labeler.yml diff --git a/.github/pr-branch-labeler.yml b/.github/pr-branch-labeler.yml new file mode 100644 index 0000000000..bf4045442a --- /dev/null +++ b/.github/pr-branch-labeler.yml @@ -0,0 +1,15 @@ +# Apply label "feature" if head matches "feature/*" +type: feature: + head: "feature/*" + +# Apply label "feature" if head matches "feature/*" +type: enhancement: + head: "enhancement/*" + +# Apply label "bugfix" if head matches one of "bugfix/*" or "hotfix/*" +type: bugfix: + head: ["bugfix/*", "hotfix/*"] + +# Apply label "release" if base matches "release/*" +Bump Minor: + base: "release/next-minor" \ No newline at end of file diff --git a/.github/pr-glob-labeler.yml b/.github/pr-glob-labeler.yml new file mode 100644 index 0000000000..367cb16625 --- /dev/null +++ b/.github/pr-glob-labeler.yml @@ -0,0 +1,7 @@ +# Add type: unittest label if any changes in tests folders +type: unittest: +- any: ['tests/**/*', 'openpype/tests/**/*'] + +# any changes in documentation structure +type: documentation: +- any: ['website/**/*', 'docs/**/*'] \ No newline at end of file diff --git a/.github/workflows/project_actions.yml b/.github/workflows/project_actions.yml index dc68a9dc0e..dfa9bdf61f 100644 --- a/.github/workflows/project_actions.yml +++ b/.github/workflows/project_actions.yml @@ -20,6 +20,7 @@ jobs: project_id: 11 resource_node_id: ${{ github.event.pull_request.node_id }} status_value: Change Requested + size-label: name: pr_size_label runs-on: ubuntu-latest @@ -42,4 +43,25 @@ jobs: "1000": "L", "1500": "XL", "2500": "XXL" - } \ No newline at end of file + } + + label_prs_branch: + name: pr_branch_label + runs-on: ubuntu-latest + if: github.event.action == 'opened' + steps: + - name: Label PRs - Branch name detection + uses: ffittschen/pr-branch-labeler@v1 + with: + repo-token: ${{ secrets.YNPUT_BOT_TOKEN }} + + label_prs_globe: + name: pr_globe_label + runs-on: ubuntu-latest + if: github.event.action == 'opened' + steps: + - name: Label PRs - Globe detection + uses: actions/labeler@v4 + with: + repo-token: ${{ secrets.YNPUT_BOT_TOKEN }} + configuration-path: ".github/pr-glob-labeler.yml" \ No newline at end of file From 14ed02e2a9eecad85372a97bad5ec4cde7872bcb Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 29 Mar 2023 15:08:22 +0200 Subject: [PATCH 196/228] adding pr glob config for hosts --- .github/pr-glob-labeler.yml | 38 ++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/.github/pr-glob-labeler.yml b/.github/pr-glob-labeler.yml index 367cb16625..6a71b1616a 100644 --- a/.github/pr-glob-labeler.yml +++ b/.github/pr-glob-labeler.yml @@ -4,4 +4,40 @@ type: unittest: # any changes in documentation structure type: documentation: -- any: ['website/**/*', 'docs/**/*'] \ No newline at end of file +- any: ['website/**/*', 'docs/**/*'] + +# hosts triage +host: Nuke: +- openpype/hosts/nuke/**/* +host: Photoshop: +- openpype/hosts/photoshop/**/* +host: Harmony: +- openpype/hosts/harmony/**/* +host: UE: +- openpype/hosts/unreal/**/* +host: Houdini: +- openpype/hosts/houdini/**/* +host: Maya: +- openpype/hosts/maya/**/* +host: Resolve: +- openpype/hosts/resolve/**/* +host: Blender: +- openpype/hosts/blender/**/* +host: Hiero: +- openpype/hosts/hiero/**/* +host: Fusion: +- openpype/hosts/fusion/**/* +host: Flame: +- openpype/hosts/flame/**/* +host: TrayPublisher: +- openpype/hosts/traypublisher/**/* +host: 3dsmax: +- openpype/hosts/max/**/* +host: TV Paint: +- openpype/hosts/tvpaint/**/* +host: CelAction: +- openpype/hosts/celaction/**/* +host: After Effects: +- openpype/hosts/aftereffects/**/* +host: Substance Painter: +- openpype/hosts/substancepainter/**/* From e7bcf5509626d321b2f2028b293da5bdbaf24c28 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 29 Mar 2023 15:23:13 +0200 Subject: [PATCH 197/228] yaml is not supporting spaces in keys --- .github/pr-branch-labeler.yml | 8 +++--- .github/pr-glob-labeler.yml | 54 +++++++++++++++++++++++------------ 2 files changed, 39 insertions(+), 23 deletions(-) diff --git a/.github/pr-branch-labeler.yml b/.github/pr-branch-labeler.yml index bf4045442a..58bcbcb72a 100644 --- a/.github/pr-branch-labeler.yml +++ b/.github/pr-branch-labeler.yml @@ -1,15 +1,15 @@ # Apply label "feature" if head matches "feature/*" -type: feature: +'type: feature': head: "feature/*" # Apply label "feature" if head matches "feature/*" -type: enhancement: +'type: enhancement': head: "enhancement/*" # Apply label "bugfix" if head matches one of "bugfix/*" or "hotfix/*" -type: bugfix: +'type: bugfix': head: ["bugfix/*", "hotfix/*"] # Apply label "release" if base matches "release/*" -Bump Minor: +'Bump Minor': base: "release/next-minor" \ No newline at end of file diff --git a/.github/pr-glob-labeler.yml b/.github/pr-glob-labeler.yml index 6a71b1616a..0c1164f659 100644 --- a/.github/pr-glob-labeler.yml +++ b/.github/pr-glob-labeler.yml @@ -1,43 +1,59 @@ # Add type: unittest label if any changes in tests folders -type: unittest: +'type: unittest': - any: ['tests/**/*', 'openpype/tests/**/*'] # any changes in documentation structure -type: documentation: +'type: documentation': - any: ['website/**/*', 'docs/**/*'] # hosts triage -host: Nuke: +'host: Nuke': - openpype/hosts/nuke/**/* -host: Photoshop: + +'host: Photoshop': - openpype/hosts/photoshop/**/* -host: Harmony: + +'host: Harmony': - openpype/hosts/harmony/**/* -host: UE: + +'host: UE': - openpype/hosts/unreal/**/* -host: Houdini: + +'host: Houdini': - openpype/hosts/houdini/**/* -host: Maya: + +'host: Maya': - openpype/hosts/maya/**/* -host: Resolve: + +'host: Resolve': - openpype/hosts/resolve/**/* -host: Blender: + +'host: Blender': - openpype/hosts/blender/**/* -host: Hiero: + +'host: Hiero': - openpype/hosts/hiero/**/* -host: Fusion: + +'host: Fusion': - openpype/hosts/fusion/**/* -host: Flame: + +'host: Flame': - openpype/hosts/flame/**/* -host: TrayPublisher: + +'host: TrayPublisher': - openpype/hosts/traypublisher/**/* -host: 3dsmax: + +'host: 3dsmax': - openpype/hosts/max/**/* -host: TV Paint: + +'host: TV Paint': - openpype/hosts/tvpaint/**/* -host: CelAction: + +'host: CelAction': - openpype/hosts/celaction/**/* -host: After Effects: + +'host: After Effects': - openpype/hosts/aftereffects/**/* -host: Substance Painter: + +'host: Substance Painter': - openpype/hosts/substancepainter/**/* From 6b94d073a9549b097ee6075aed9c39107ec6252b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 29 Mar 2023 15:36:32 +0200 Subject: [PATCH 198/228] updating project actions --- .github/pr-glob-labeler.yml | 10 ++++++++-- .github/workflows/project_actions.yml | 8 ++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/.github/pr-glob-labeler.yml b/.github/pr-glob-labeler.yml index 0c1164f659..90e497fbd5 100644 --- a/.github/pr-glob-labeler.yml +++ b/.github/pr-glob-labeler.yml @@ -1,10 +1,16 @@ # Add type: unittest label if any changes in tests folders 'type: unittest': -- any: ['tests/**/*', 'openpype/tests/**/*'] +- tests/**/* +- tests/** +- openpype/tests/**/* +- openpype/tests/** # any changes in documentation structure 'type: documentation': -- any: ['website/**/*', 'docs/**/*'] +- website/**/* +- website/** +- docs/**/* +- docs/** # hosts triage 'host: Nuke': diff --git a/.github/workflows/project_actions.yml b/.github/workflows/project_actions.yml index dfa9bdf61f..ad8e8b9e13 100644 --- a/.github/workflows/project_actions.yml +++ b/.github/workflows/project_actions.yml @@ -48,7 +48,9 @@ jobs: label_prs_branch: name: pr_branch_label runs-on: ubuntu-latest - if: github.event.action == 'opened' + if: | + ${{(github.event_name == 'pull_request' && github.event.action == 'synchronize') + || (github.event_name == 'pull_request' && github.event.action == 'opened')}} steps: - name: Label PRs - Branch name detection uses: ffittschen/pr-branch-labeler@v1 @@ -58,7 +60,9 @@ jobs: label_prs_globe: name: pr_globe_label runs-on: ubuntu-latest - if: github.event.action == 'opened' + if: | + ${{(github.event_name == 'pull_request' && github.event.action == 'synchronize') + || (github.event_name == 'pull_request' && github.event.action == 'opened')}} steps: - name: Label PRs - Globe detection uses: actions/labeler@v4 From 081cd507791703b23fed0d1031f966986bad1e76 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 29 Mar 2023 16:02:25 +0200 Subject: [PATCH 199/228] updating pr globe labeler config adding modules --- .github/pr-glob-labeler.yml | 42 +++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/.github/pr-glob-labeler.yml b/.github/pr-glob-labeler.yml index 90e497fbd5..f990c6dfbe 100644 --- a/.github/pr-glob-labeler.yml +++ b/.github/pr-glob-labeler.yml @@ -15,51 +15,93 @@ # hosts triage 'host: Nuke': - openpype/hosts/nuke/**/* +- openpype/hosts/nuke/** 'host: Photoshop': - openpype/hosts/photoshop/**/* +- openpype/hosts/photoshop/** 'host: Harmony': - openpype/hosts/harmony/**/* +- openpype/hosts/harmony/** 'host: UE': - openpype/hosts/unreal/**/* +- openpype/hosts/unreal/** 'host: Houdini': - openpype/hosts/houdini/**/* +- openpype/hosts/houdini/** 'host: Maya': - openpype/hosts/maya/**/* +- openpype/hosts/maya/** 'host: Resolve': - openpype/hosts/resolve/**/* +- openpype/hosts/resolve/** 'host: Blender': - openpype/hosts/blender/**/* +- openpype/hosts/blender/** 'host: Hiero': - openpype/hosts/hiero/**/* +- openpype/hosts/hiero/** 'host: Fusion': - openpype/hosts/fusion/**/* +- openpype/hosts/fusion/** 'host: Flame': - openpype/hosts/flame/**/* +- openpype/hosts/flame/** 'host: TrayPublisher': - openpype/hosts/traypublisher/**/* +- openpype/hosts/traypublisher/** 'host: 3dsmax': - openpype/hosts/max/**/* +- openpype/hosts/max/** 'host: TV Paint': - openpype/hosts/tvpaint/**/* +- openpype/hosts/tvpaint/** 'host: CelAction': - openpype/hosts/celaction/**/* +- openpype/hosts/celaction/** 'host: After Effects': - openpype/hosts/aftereffects/**/* +- openpype/hosts/aftereffects/** 'host: Substance Painter': - openpype/hosts/substancepainter/**/* +- openpype/hosts/substancepainter/** + +# modules triage +'module: Deadline': +- openpype/modules/deadline/**/* +- openpype/modules/deadline/** + +'module: RoyalRender': +- openpype/modules/royalrender/**/* +- openpype/modules/royalrender/** + +'module: Sitesync': +- openpype/modules/sync_server/**/* +- openpype/modules/sync_server/** + +'module: Ftrack': +- openpype/modules/ftrack/**/* +- openpype/modules/ftrack/** + +'module: Shotgrid': +- openpype/modules/shotgrid/**/* +- openpype/modules/shotgrid/** + +'module: Kitsu': +- openpype/modules/kitsu/**/* +- openpype/modules/kitsu/** From 711089fc9cc7d0c1b9ccc6335ec67f3632042af8 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 29 Mar 2023 16:29:35 +0200 Subject: [PATCH 200/228] improving glob expressions to include settings --- .github/pr-glob-labeler.yml | 103 +++++++++++++++++------------------- 1 file changed, 49 insertions(+), 54 deletions(-) diff --git a/.github/pr-glob-labeler.yml b/.github/pr-glob-labeler.yml index f990c6dfbe..e05421f5d7 100644 --- a/.github/pr-glob-labeler.yml +++ b/.github/pr-glob-labeler.yml @@ -1,107 +1,102 @@ # Add type: unittest label if any changes in tests folders 'type: unittest': -- tests/**/* -- tests/** -- openpype/tests/**/* -- openpype/tests/** +- /**/*tests*/**/* # any changes in documentation structure 'type: documentation': -- website/**/* -- website/** -- docs/**/* -- docs/** +- /**/*website*/**/* +- /**/*docs*/**/* # hosts triage 'host: Nuke': -- openpype/hosts/nuke/**/* -- openpype/hosts/nuke/** +- /**/*nuke* +- /**/*nuke*/**/* 'host: Photoshop': -- openpype/hosts/photoshop/**/* -- openpype/hosts/photoshop/** +- /**/*photoshop* +- /**/*photoshop*/**/* 'host: Harmony': -- openpype/hosts/harmony/**/* -- openpype/hosts/harmony/** +- /**/*harmony* +- /**/*harmony*/**/* 'host: UE': -- openpype/hosts/unreal/**/* -- openpype/hosts/unreal/** +- /**/*unreal* +- /**/*unreal*/**/* 'host: Houdini': -- openpype/hosts/houdini/**/* -- openpype/hosts/houdini/** +- /**/*houdini* +- /**/*houdini*/**/* 'host: Maya': -- openpype/hosts/maya/**/* -- openpype/hosts/maya/** +- /**/*maya* +- /**/*maya*/**/* 'host: Resolve': -- openpype/hosts/resolve/**/* -- openpype/hosts/resolve/** +- /**/*resolve* +- /**/*resolve*/**/* 'host: Blender': -- openpype/hosts/blender/**/* -- openpype/hosts/blender/** +- /**/*blender* +- /**/*blender*/**/* 'host: Hiero': -- openpype/hosts/hiero/**/* -- openpype/hosts/hiero/** +- /**/*hiero* +- /**/*hiero*/**/* 'host: Fusion': -- openpype/hosts/fusion/**/* -- openpype/hosts/fusion/** +- /**/*fusion* +- /**/*fusion*/**/* 'host: Flame': -- openpype/hosts/flame/**/* -- openpype/hosts/flame/** +- /**/*flame* +- /**/*flame*/**/* 'host: TrayPublisher': -- openpype/hosts/traypublisher/**/* -- openpype/hosts/traypublisher/** +- /**/*traypublisher* +- /**/*traypublisher*/**/* 'host: 3dsmax': -- openpype/hosts/max/**/* -- openpype/hosts/max/** +- /**/*max* +- /**/*max*/**/* 'host: TV Paint': -- openpype/hosts/tvpaint/**/* -- openpype/hosts/tvpaint/** +- /**/*tvpaint* +- /**/*tvpaint*/**/* 'host: CelAction': -- openpype/hosts/celaction/**/* -- openpype/hosts/celaction/** +- /**/*celaction* +- /**/*celaction*/**/* 'host: After Effects': -- openpype/hosts/aftereffects/**/* -- openpype/hosts/aftereffects/** +- /**/*aftereffects* +- /**/*aftereffects*/**/* 'host: Substance Painter': -- openpype/hosts/substancepainter/**/* -- openpype/hosts/substancepainter/** +- /**/*substancepainter* +- /**/*substancepainter*/**/* # modules triage 'module: Deadline': -- openpype/modules/deadline/**/* -- openpype/modules/deadline/** +- /**/*deadline* +- /**/*deadline*/**/* 'module: RoyalRender': -- openpype/modules/royalrender/**/* -- openpype/modules/royalrender/** +- /**/*royalrender* +- /**/*royalrender*/**/* 'module: Sitesync': -- openpype/modules/sync_server/**/* -- openpype/modules/sync_server/** +- /**/*sync_server* +- /**/*sync_server*/**/* 'module: Ftrack': -- openpype/modules/ftrack/**/* -- openpype/modules/ftrack/** +- /**/*ftrack* +- /**/*ftrack*/**/* 'module: Shotgrid': -- openpype/modules/shotgrid/**/* -- openpype/modules/shotgrid/** +- /**/*shotgrid* +- /**/*shotgrid*/**/* 'module: Kitsu': -- openpype/modules/kitsu/**/* -- openpype/modules/kitsu/** +- /**/*kitsu* +- /**/*kitsu*/**/* From 14cfe2b9938998a91e15a12cb5bb8092b567405d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 29 Mar 2023 17:12:41 +0200 Subject: [PATCH 201/228] project action with pr target trigger --- .github/workflows/project_actions.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/project_actions.yml b/.github/workflows/project_actions.yml index ad8e8b9e13..4fc32d9986 100644 --- a/.github/workflows/project_actions.yml +++ b/.github/workflows/project_actions.yml @@ -1,7 +1,7 @@ name: project-actions on: - pull_request: + pull_request_target: types: [opened, synchronize, assigned, review_requested] pull_request_review: types: [submitted] From 5ccc8cd745c0c0cc83480eed4ee835b3f82f757e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 29 Mar 2023 17:30:38 +0200 Subject: [PATCH 202/228] fixing target to include pull_request also fix globes --- .github/pr-glob-labeler.yml | 98 +++++++++++++-------------- .github/workflows/project_actions.yml | 1 + 2 files changed, 50 insertions(+), 49 deletions(-) diff --git a/.github/pr-glob-labeler.yml b/.github/pr-glob-labeler.yml index e05421f5d7..f23f74f310 100644 --- a/.github/pr-glob-labeler.yml +++ b/.github/pr-glob-labeler.yml @@ -1,102 +1,102 @@ # Add type: unittest label if any changes in tests folders 'type: unittest': -- /**/*tests*/**/* +- **/*tests*/**/* # any changes in documentation structure 'type: documentation': -- /**/*website*/**/* -- /**/*docs*/**/* +- **/*website*/**/* +- **/*docs*/**/* # hosts triage 'host: Nuke': -- /**/*nuke* -- /**/*nuke*/**/* +- **/*nuke* +- **/*nuke*/**/* 'host: Photoshop': -- /**/*photoshop* -- /**/*photoshop*/**/* +- **/*photoshop* +- **/*photoshop*/**/* 'host: Harmony': -- /**/*harmony* -- /**/*harmony*/**/* +- **/*harmony* +- **/*harmony*/**/* 'host: UE': -- /**/*unreal* -- /**/*unreal*/**/* +- **/*unreal* +- **/*unreal*/**/* 'host: Houdini': -- /**/*houdini* -- /**/*houdini*/**/* +- **/*houdini* +- **/*houdini*/**/* 'host: Maya': -- /**/*maya* -- /**/*maya*/**/* +- **/*maya* +- **/*maya*/**/* 'host: Resolve': -- /**/*resolve* -- /**/*resolve*/**/* +- **/*resolve* +- **/*resolve*/**/* 'host: Blender': -- /**/*blender* -- /**/*blender*/**/* +- **/*blender* +- **/*blender*/**/* 'host: Hiero': -- /**/*hiero* -- /**/*hiero*/**/* +- **/*hiero* +- **/*hiero*/**/* 'host: Fusion': -- /**/*fusion* -- /**/*fusion*/**/* +- **/*fusion* +- **/*fusion*/**/* 'host: Flame': -- /**/*flame* -- /**/*flame*/**/* +- **/*flame* +- **/*flame*/**/* 'host: TrayPublisher': -- /**/*traypublisher* -- /**/*traypublisher*/**/* +- **/*traypublisher* +- **/*traypublisher*/**/* 'host: 3dsmax': -- /**/*max* -- /**/*max*/**/* +- **/*max* +- **/*max*/**/* 'host: TV Paint': -- /**/*tvpaint* -- /**/*tvpaint*/**/* +- **/*tvpaint* +- **/*tvpaint*/**/* 'host: CelAction': -- /**/*celaction* -- /**/*celaction*/**/* +- **/*celaction* +- **/*celaction*/**/* 'host: After Effects': -- /**/*aftereffects* -- /**/*aftereffects*/**/* +- **/*aftereffects* +- **/*aftereffects*/**/* 'host: Substance Painter': -- /**/*substancepainter* -- /**/*substancepainter*/**/* +- **/*substancepainter* +- **/*substancepainter*/**/* # modules triage 'module: Deadline': -- /**/*deadline* -- /**/*deadline*/**/* +- **/*deadline* +- **/*deadline*/**/* 'module: RoyalRender': -- /**/*royalrender* -- /**/*royalrender*/**/* +- **/*royalrender* +- **/*royalrender*/**/* 'module: Sitesync': -- /**/*sync_server* -- /**/*sync_server*/**/* +- **/*sync_server* +- **/*sync_server*/**/* 'module: Ftrack': -- /**/*ftrack* -- /**/*ftrack*/**/* +- **/*ftrack* +- **/*ftrack*/**/* 'module: Shotgrid': -- /**/*shotgrid* -- /**/*shotgrid*/**/* +- **/*shotgrid* +- **/*shotgrid*/**/* 'module: Kitsu': -- /**/*kitsu* -- /**/*kitsu*/**/* +- **/*kitsu* +- **/*kitsu*/**/* diff --git a/.github/workflows/project_actions.yml b/.github/workflows/project_actions.yml index 4fc32d9986..605a0ae422 100644 --- a/.github/workflows/project_actions.yml +++ b/.github/workflows/project_actions.yml @@ -2,6 +2,7 @@ name: project-actions on: pull_request_target: + pull_request: types: [opened, synchronize, assigned, review_requested] pull_request_review: types: [submitted] From fb1d4ea5b1e401cd7ab3b320dfcab14dd5fdc665 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Wed, 29 Mar 2023 17:35:48 +0200 Subject: [PATCH 203/228] removing pr target --- .github/workflows/project_actions.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/project_actions.yml b/.github/workflows/project_actions.yml index 605a0ae422..c66e378a5a 100644 --- a/.github/workflows/project_actions.yml +++ b/.github/workflows/project_actions.yml @@ -1,7 +1,6 @@ name: project-actions on: - pull_request_target: pull_request: types: [opened, synchronize, assigned, review_requested] pull_request_review: @@ -69,4 +68,4 @@ jobs: uses: actions/labeler@v4 with: repo-token: ${{ secrets.YNPUT_BOT_TOKEN }} - configuration-path: ".github/pr-glob-labeler.yml" \ No newline at end of file + configuration-path: ".github/pr-glob-labeler.yml" From 911c089319b7b90fc6e5726555d9fc9aa5d21a80 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 29 Mar 2023 17:54:17 +0200 Subject: [PATCH 204/228] Fix collect current file - Fix families filtering - Remove unused import - Fix correct detection if scene is new but unsaved scene --- .../hosts/houdini/plugins/publish/collect_current_file.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_current_file.py b/openpype/hosts/houdini/plugins/publish/collect_current_file.py index 9cca07fdc7..caf679f98b 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_current_file.py +++ b/openpype/hosts/houdini/plugins/publish/collect_current_file.py @@ -1,7 +1,6 @@ import os import hou -from openpype.pipeline import legacy_io import pyblish.api @@ -11,7 +10,7 @@ class CollectHoudiniCurrentFile(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder - 0.01 label = "Houdini Current File" hosts = ["houdini"] - family = ["workfile"] + families = ["workfile"] def process(self, instance): """Inject the current working file""" @@ -21,7 +20,7 @@ class CollectHoudiniCurrentFile(pyblish.api.InstancePlugin): # By default, Houdini will even point a new scene to a path. # However if the file is not saved at all and does not exist, # we assume the user never set it. - filepath = "" + current_file = "" elif os.path.basename(current_file) == "untitled.hip": # Due to even a new file being called 'untitled.hip' we are unable From 6e04f620d949d80e20c315694cead3c0dc71d913 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 29 Mar 2023 22:06:14 +0200 Subject: [PATCH 205/228] project action fix glob path --- .github/pr-glob-labeler.yml | 98 ++++++++++++++++++------------------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/.github/pr-glob-labeler.yml b/.github/pr-glob-labeler.yml index f23f74f310..69df5ace75 100644 --- a/.github/pr-glob-labeler.yml +++ b/.github/pr-glob-labeler.yml @@ -1,102 +1,102 @@ # Add type: unittest label if any changes in tests folders 'type: unittest': -- **/*tests*/**/* +- ./**/*tests*/**/* # any changes in documentation structure 'type: documentation': -- **/*website*/**/* -- **/*docs*/**/* +- /**/*website*/**/* +- /**/*docs*/**/* # hosts triage 'host: Nuke': -- **/*nuke* -- **/*nuke*/**/* +- /**/*nuke* +- ./**/*nuke*/**/* 'host: Photoshop': -- **/*photoshop* -- **/*photoshop*/**/* +- ./**/*photoshop* +- ./**/*photoshop*/**/* 'host: Harmony': -- **/*harmony* -- **/*harmony*/**/* +- ./**/*harmony* +- ./**/*harmony*/**/* 'host: UE': -- **/*unreal* -- **/*unreal*/**/* +- ./**/*unreal* +- ./**/*unreal*/**/* 'host: Houdini': -- **/*houdini* -- **/*houdini*/**/* +- ./**/*houdini* +- ./**/*houdini*/**/* 'host: Maya': -- **/*maya* -- **/*maya*/**/* +- ./**/*maya* +- ./**/*maya*/**/* 'host: Resolve': -- **/*resolve* -- **/*resolve*/**/* +- ./**/*resolve* +- ./**/*resolve*/**/* 'host: Blender': -- **/*blender* -- **/*blender*/**/* +- ./**/*blender* +- ./**/*blender*/**/* 'host: Hiero': -- **/*hiero* -- **/*hiero*/**/* +- ./**/*hiero* +- ./**/*hiero*/**/* 'host: Fusion': -- **/*fusion* -- **/*fusion*/**/* +- ./**/*fusion* +- ./**/*fusion*/**/* 'host: Flame': -- **/*flame* -- **/*flame*/**/* +- ./**/*flame* +- ./**/*flame*/**/* 'host: TrayPublisher': -- **/*traypublisher* -- **/*traypublisher*/**/* +- ./**/*traypublisher* +- ./**/*traypublisher*/**/* 'host: 3dsmax': -- **/*max* -- **/*max*/**/* +- ./**/*max* +- ./**/*max*/**/* 'host: TV Paint': -- **/*tvpaint* -- **/*tvpaint*/**/* +- ./**/*tvpaint* +- ./**/*tvpaint*/**/* 'host: CelAction': -- **/*celaction* -- **/*celaction*/**/* +- ./**/*celaction* +- ./**/*celaction*/**/* 'host: After Effects': -- **/*aftereffects* -- **/*aftereffects*/**/* +- ./**/*aftereffects* +- ./**/*aftereffects*/**/* 'host: Substance Painter': -- **/*substancepainter* -- **/*substancepainter*/**/* +- ./**/*substancepainter* +- ./**/*substancepainter*/**/* # modules triage 'module: Deadline': -- **/*deadline* -- **/*deadline*/**/* +- ./**/*deadline* +- ./**/*deadline*/**/* 'module: RoyalRender': -- **/*royalrender* -- **/*royalrender*/**/* +- ./**/*royalrender* +- ./**/*royalrender*/**/* 'module: Sitesync': -- **/*sync_server* -- **/*sync_server*/**/* +- ./**/*sync_server* +- ./**/*sync_server*/**/* 'module: Ftrack': -- **/*ftrack* -- **/*ftrack*/**/* +- ./**/*ftrack* +- ./**/*ftrack*/**/* 'module: Shotgrid': -- **/*shotgrid* -- **/*shotgrid*/**/* +- ./**/*shotgrid* +- ./**/*shotgrid*/**/* 'module: Kitsu': -- **/*kitsu* -- **/*kitsu*/**/* +- ./**/*kitsu* +- ./**/*kitsu*/**/* From 9dd6b3bbe225065d66d0d77afa71ef2db2460b14 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 29 Mar 2023 22:11:42 +0200 Subject: [PATCH 206/228] fix the glob expression --- .github/pr-glob-labeler.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/pr-glob-labeler.yml b/.github/pr-glob-labeler.yml index 69df5ace75..f5eae1067e 100644 --- a/.github/pr-glob-labeler.yml +++ b/.github/pr-glob-labeler.yml @@ -1,15 +1,16 @@ # Add type: unittest label if any changes in tests folders 'type: unittest': -- ./**/*tests*/**/* +- /tests/**/* +- openpype/tests/**/* # any changes in documentation structure 'type: documentation': -- /**/*website*/**/* -- /**/*docs*/**/* +- '*/**/*website*/**/*' +- '*/**/*docs*/**/*' # hosts triage 'host: Nuke': -- /**/*nuke* +- ./**/*nuke* - ./**/*nuke*/**/* 'host: Photoshop': From 07f09b0fd691f59db61dea7dd125777c4a18ad90 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 29 Mar 2023 22:15:35 +0200 Subject: [PATCH 207/228] glob final fix of expressions with yaml compatibilty --- .github/pr-glob-labeler.yml | 95 ++++++++++++++++++------------------- 1 file changed, 47 insertions(+), 48 deletions(-) diff --git a/.github/pr-glob-labeler.yml b/.github/pr-glob-labeler.yml index f5eae1067e..286e7768b5 100644 --- a/.github/pr-glob-labeler.yml +++ b/.github/pr-glob-labeler.yml @@ -1,7 +1,6 @@ # Add type: unittest label if any changes in tests folders 'type: unittest': -- /tests/**/* -- openpype/tests/**/* +- '*/*tests*/**/*' # any changes in documentation structure 'type: documentation': @@ -10,94 +9,94 @@ # hosts triage 'host: Nuke': -- ./**/*nuke* -- ./**/*nuke*/**/* +- '*/**/*nuke*' +- '*/**/*nuke*/**/*' 'host: Photoshop': -- ./**/*photoshop* -- ./**/*photoshop*/**/* +- '*/**/*photoshop*' +- '*/**/*photoshop*/**/*' 'host: Harmony': -- ./**/*harmony* -- ./**/*harmony*/**/* +- '*/**/*harmony*' +- '*/**/*harmony*/**/*' 'host: UE': -- ./**/*unreal* -- ./**/*unreal*/**/* +- '*/**/*unreal*' +- '*/**/*unreal*/**/*' 'host: Houdini': -- ./**/*houdini* -- ./**/*houdini*/**/* +- '*/**/*houdini*' +- '*/**/*houdini*/**/*' 'host: Maya': -- ./**/*maya* -- ./**/*maya*/**/* +- '*/**/*maya*' +- '*/**/*maya*/**/*' 'host: Resolve': -- ./**/*resolve* -- ./**/*resolve*/**/* +- '*/**/*resolve*' +- '*/**/*resolve*/**/*' 'host: Blender': -- ./**/*blender* -- ./**/*blender*/**/* +- '*/**/*blender*' +- '*/**/*blender*/**/*' 'host: Hiero': -- ./**/*hiero* -- ./**/*hiero*/**/* +- '*/**/*hiero*' +- '*/**/*hiero*/**/*' 'host: Fusion': -- ./**/*fusion* -- ./**/*fusion*/**/* +- '*/**/*fusion*' +- '*/**/*fusion*/**/*' 'host: Flame': -- ./**/*flame* -- ./**/*flame*/**/* +- '*/**/*flame*' +- '*/**/*flame*/**/*' 'host: TrayPublisher': -- ./**/*traypublisher* -- ./**/*traypublisher*/**/* +- '*/**/*traypublisher*' +- '*/**/*traypublisher*/**/*' 'host: 3dsmax': -- ./**/*max* -- ./**/*max*/**/* +- '*/**/*max*' +- '*/**/*max*/**/*' 'host: TV Paint': -- ./**/*tvpaint* -- ./**/*tvpaint*/**/* +- '*/**/*tvpaint*' +- '*/**/*tvpaint*/**/*' 'host: CelAction': -- ./**/*celaction* -- ./**/*celaction*/**/* +- '*/**/*celaction*' +- '*/**/*celaction*/**/*' 'host: After Effects': -- ./**/*aftereffects* -- ./**/*aftereffects*/**/* +- '*/**/*aftereffects*' +- '*/**/*aftereffects*/**/*' 'host: Substance Painter': -- ./**/*substancepainter* -- ./**/*substancepainter*/**/* +- '*/**/*substancepainter*' +- '*/**/*substancepainter*/**/*' # modules triage 'module: Deadline': -- ./**/*deadline* -- ./**/*deadline*/**/* +- '*/**/*deadline*' +- '*/**/*deadline*/**/*' 'module: RoyalRender': -- ./**/*royalrender* -- ./**/*royalrender*/**/* +- '*/**/*royalrender*' +- '*/**/*royalrender*/**/*' 'module: Sitesync': -- ./**/*sync_server* -- ./**/*sync_server*/**/* +- '*/**/*sync_server*' +- '*/**/*sync_server*/**/*' 'module: Ftrack': -- ./**/*ftrack* -- ./**/*ftrack*/**/* +- '*/**/*ftrack*' +- '*/**/*ftrack*/**/*' 'module: Shotgrid': -- ./**/*shotgrid* -- ./**/*shotgrid*/**/* +- '*/**/*shotgrid*' +- '*/**/*shotgrid*/**/*' 'module: Kitsu': -- ./**/*kitsu* -- ./**/*kitsu*/**/* +- '*/**/*kitsu*' +- '*/**/*kitsu*/**/*' From 9a2ef0738f1c26a9abbe5d64fa46a8584173f568 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 29 Mar 2023 22:19:42 +0200 Subject: [PATCH 208/228] bug label rather then bugfix --- .github/pr-branch-labeler.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/pr-branch-labeler.yml b/.github/pr-branch-labeler.yml index 58bcbcb72a..ca82051006 100644 --- a/.github/pr-branch-labeler.yml +++ b/.github/pr-branch-labeler.yml @@ -7,7 +7,7 @@ head: "enhancement/*" # Apply label "bugfix" if head matches one of "bugfix/*" or "hotfix/*" -'type: bugfix': +'type: bug': head: ["bugfix/*", "hotfix/*"] # Apply label "release" if base matches "release/*" From 2b540f1b2e0b6fbd9642d50bc4a81c94b3e58a89 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 29 Mar 2023 22:26:59 +0200 Subject: [PATCH 209/228] project action trigger to target pr --- .github/workflows/project_actions.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/project_actions.yml b/.github/workflows/project_actions.yml index c66e378a5a..769f40e0b8 100644 --- a/.github/workflows/project_actions.yml +++ b/.github/workflows/project_actions.yml @@ -1,7 +1,7 @@ name: project-actions on: - pull_request: + pull_request_target: types: [opened, synchronize, assigned, review_requested] pull_request_review: types: [submitted] From 36f1a1103ee6e148c0dd42aa66f81fff05b352cf Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 30 Mar 2023 00:23:43 +0200 Subject: [PATCH 210/228] Fix #4693: Add `displayGradient` key to allow disabling gradient background --- openpype/hosts/maya/api/lib.py | 4 ++-- openpype/settings/defaults/project_settings/maya.json | 3 ++- .../projects_schema/schemas/schema_maya_capture.json | 10 +++++++--- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index aa1e501578..1a62e7dbc3 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -2478,8 +2478,8 @@ def load_capture_preset(data=None): float(value[2]) / 255 ] disp_options[key] = value - else: - disp_options['displayGradient'] = True + elif key == "displayGradient": + disp_options[key] = value options['display_options'] = disp_options diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index e914eb29f9..fda053e6e6 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -795,6 +795,7 @@ "quality": 95 }, "Display Options": { + "override_display": true, "background": [ 125, 125, @@ -813,7 +814,7 @@ 125, 255 ], - "override_display": true + "displayGradient": true }, "Generic": { "isolate_view": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json index 416e530db2..a4a986bad8 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json @@ -48,7 +48,11 @@ "type": "label", "label": "Display Options" }, - + { + "type": "boolean", + "key": "override_display", + "label": "Override display options" + }, { "type": "color", "key": "background", @@ -66,8 +70,8 @@ }, { "type": "boolean", - "key": "override_display", - "label": "Override display options" + "key": "displayGradient", + "label": "Display background gradient" } ] }, From 4ee44bcfec4b06f2ba95881261de7a79b0ae99a1 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 30 Mar 2023 09:15:48 +0200 Subject: [PATCH 211/228] update project action workflow --- .github/workflows/project_actions.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/project_actions.yml b/.github/workflows/project_actions.yml index 769f40e0b8..4c09549ac4 100644 --- a/.github/workflows/project_actions.yml +++ b/.github/workflows/project_actions.yml @@ -69,3 +69,4 @@ jobs: with: repo-token: ${{ secrets.YNPUT_BOT_TOKEN }} configuration-path: ".github/pr-glob-labeler.yml" + sync-labels: false From b752035022dd21abdf192a74084aefd6dfcf276f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 30 Mar 2023 09:19:05 +0200 Subject: [PATCH 212/228] project actions sync labels true --- .github/workflows/project_actions.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/project_actions.yml b/.github/workflows/project_actions.yml index 4c09549ac4..8080d68156 100644 --- a/.github/workflows/project_actions.yml +++ b/.github/workflows/project_actions.yml @@ -69,4 +69,4 @@ jobs: with: repo-token: ${{ secrets.YNPUT_BOT_TOKEN }} configuration-path: ".github/pr-glob-labeler.yml" - sync-labels: false + sync-labels: true From c3e4bc6409a91d88a481dffa98ecc9ff1454e32a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 30 Mar 2023 09:29:55 +0200 Subject: [PATCH 213/228] project actions sync labels off update labeler --- .github/workflows/project_actions.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/project_actions.yml b/.github/workflows/project_actions.yml index 8080d68156..cfee140541 100644 --- a/.github/workflows/project_actions.yml +++ b/.github/workflows/project_actions.yml @@ -65,8 +65,8 @@ jobs: || (github.event_name == 'pull_request' && github.event.action == 'opened')}} steps: - name: Label PRs - Globe detection - uses: actions/labeler@v4 + uses: actions/labeler@v4.0.3 with: repo-token: ${{ secrets.YNPUT_BOT_TOKEN }} configuration-path: ".github/pr-glob-labeler.yml" - sync-labels: true + sync-labels: false From 18efd5276df132909d8eae592f09cd6d7b011cbb Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 30 Mar 2023 11:57:36 +0200 Subject: [PATCH 214/228] :art: add camera loader --- .../hosts/maya/plugins/load/load_camera.py | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 openpype/hosts/maya/plugins/load/load_camera.py diff --git a/openpype/hosts/maya/plugins/load/load_camera.py b/openpype/hosts/maya/plugins/load/load_camera.py new file mode 100644 index 0000000000..5e26e8c02f --- /dev/null +++ b/openpype/hosts/maya/plugins/load/load_camera.py @@ -0,0 +1,76 @@ +import openpype.hosts.maya.api.plugin +from openpype.hosts.maya.api.lib import get_container_members + + +class CameraLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): + """Reference Camera""" + + families = ["camera", "camerarig"] + label = "Reference camera" + representations = ["abc", "ma"] + order = -10 + icon = "code-fork" + color = "orange" + + def process_reference(self, context, name, namespace, data): + + import maya.cmds as cmds + # Get family type from the context + + cmds.loadPlugin("AbcImport.mll", quiet=True) + nodes = cmds.file(self.fname, + namespace=namespace, + sharedReferenceFile=False, + groupReference=True, + groupName="{}:{}".format(namespace, name), + reference=True, + returnNewNodes=True) + + cameras = cmds.ls(nodes, type="camera") + + # Check the Maya version, lockTransform has been introduced since + # Maya 2016.5 Ext 2 + version = int(cmds.about(version=True)) + if version >= 2016: + for camera in cameras: + cmds.camera(camera, edit=True, lockTransform=True) + else: + self.log.warning("This version of Maya does not support locking of" + " transforms of cameras.") + + self[:] = nodes + + return nodes + + def update(self, container, representation): + + from maya import cmds + + # Get the modelPanels that used the old camera + members = get_container_members(container) + old_cameras = cmds.ls(members, type="camera", long=True) + update_panels = [] + for panel in cmds.getPanel(type="modelPanel"): + cam = cmds.ls(cmds.modelPanel(panel, query=True, camera=True), + long=True) + + # Often but not always maya returns the transform from the + # modelPanel as opposed to the camera shape, so we convert it + # to explicitly be the camera shape + if cmds.nodeType(cam) != "camera": + cam = cmds.listRelatives(cam, + children=True, + fullPath=True, + type="camera")[0] + if cam in old_cameras: + update_panels.append(panel) + + # Perform regular reference update + super(CameraLoader, self).update(container, representation) + + # Update the modelPanels to contain the new camera + members = get_container_members(container) + new_cameras = cmds.ls(members, type="camera", long=True) + new_camera = new_cameras[0] + for panel in update_panels: + cmds.modelPanel(panel, edit=True, camera=new_camera) From ea2490ce5618f556d8374e488364cabefea51a7a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 30 Mar 2023 13:37:52 +0200 Subject: [PATCH 215/228] project action - reducing job triggering --- .github/workflows/project_actions.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/project_actions.yml b/.github/workflows/project_actions.yml index cfee140541..1e1a1441f7 100644 --- a/.github/workflows/project_actions.yml +++ b/.github/workflows/project_actions.yml @@ -25,8 +25,8 @@ jobs: name: pr_size_label runs-on: ubuntu-latest if: | - ${{(github.event_name == 'pull_request' && github.event.action == 'synchronize') - || (github.event_name == 'pull_request' && github.event.action == 'assigned')}} + ${{(github.event_name == 'pull_request' && github.event.action == 'assigned') + || (github.event_name == 'pull_request' && github.event.action == 'opened')}} steps: - name: Add size label @@ -49,7 +49,7 @@ jobs: name: pr_branch_label runs-on: ubuntu-latest if: | - ${{(github.event_name == 'pull_request' && github.event.action == 'synchronize') + ${{(github.event_name == 'pull_request' && github.event.action == 'assigned') || (github.event_name == 'pull_request' && github.event.action == 'opened')}} steps: - name: Label PRs - Branch name detection @@ -61,7 +61,7 @@ jobs: name: pr_globe_label runs-on: ubuntu-latest if: | - ${{(github.event_name == 'pull_request' && github.event.action == 'synchronize') + ${{(github.event_name == 'pull_request' && github.event.action == 'assigned') || (github.event_name == 'pull_request' && github.event.action == 'opened')}} steps: - name: Label PRs - Globe detection From 3fae1f852194ee0b7ce69ca584c2f4605a3e1147 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 30 Mar 2023 13:59:22 +0200 Subject: [PATCH 216/228] Just some grammar tweaks --- openpype/client/entities.py | 8 +-- openpype/client/notes.md | 4 +- openpype/client/operations.py | 10 ++-- openpype/host/dirmap.py | 4 +- openpype/host/host.py | 6 +-- openpype/host/interfaces.py | 8 +-- .../api/extension/jsx/hostscript.jsx | 2 +- openpype/hosts/aftereffects/api/ws_stub.py | 2 +- openpype/hosts/blender/api/ops.py | 2 +- .../plugins/publish/extract_playblast.py | 2 +- openpype/hosts/flame/api/lib.py | 4 +- openpype/hosts/flame/api/pipeline.py | 2 +- openpype/hosts/flame/api/plugin.py | 54 +++++++++---------- .../hosts/flame/api/scripts/wiretap_com.py | 2 +- openpype/hosts/flame/api/utils.py | 2 +- openpype/hosts/flame/hooks/pre_flame_setup.py | 2 +- .../flame/plugins/create/create_shot_clip.py | 2 +- .../hosts/flame/plugins/load/load_clip.py | 6 +-- .../flame/plugins/load/load_clip_batch.py | 8 +-- .../publish/collect_timeline_instances.py | 4 +- .../publish/extract_subset_resources.py | 4 +- .../plugins/publish/integrate_batch_group.py | 2 +- openpype/hosts/harmony/api/README.md | 8 +-- openpype/hosts/harmony/api/TB_sceneOpened.js | 10 ++-- openpype/hosts/harmony/api/lib.py | 6 +-- openpype/hosts/harmony/api/server.py | 2 +- .../harmony/vendor/OpenHarmony/README.md | 6 +-- .../harmony/vendor/OpenHarmony/openHarmony.js | 8 +-- .../openHarmony/openHarmony_actions.js | 6 +-- .../openHarmony/openHarmony_application.js | 6 +-- .../openHarmony/openHarmony_attribute.js | 8 +-- .../openHarmony/openHarmony_backdrop.js | 4 +- .../openHarmony/openHarmony_color.js | 6 +-- .../openHarmony/openHarmony_column.js | 4 +- .../openHarmony/openHarmony_database.js | 4 +- .../openHarmony/openHarmony_dialog.js | 18 +++---- .../openHarmony/openHarmony_drawing.js | 18 +++---- .../openHarmony/openHarmony_element.js | 4 +- .../openHarmony/openHarmony_file.js | 6 +-- .../openHarmony/openHarmony_frame.js | 10 ++-- .../openHarmony/openHarmony_list.js | 6 +-- .../openHarmony/openHarmony_math.js | 16 +++--- .../openHarmony/openHarmony_metadata.js | 4 +- .../openHarmony/openHarmony_misc.js | 6 +-- .../openHarmony/openHarmony_network.js | 8 +-- .../openHarmony/openHarmony_node.js | 22 ++++---- .../openHarmony/openHarmony_nodeLink.js | 24 ++++----- .../vendor/OpenHarmony/openHarmony_tools.js | 8 +-- .../harmony/vendor/OpenHarmony/package.json | 2 +- openpype/hosts/hiero/api/__init__.py | 2 +- openpype/hosts/hiero/api/pipeline.py | 4 +- openpype/hosts/hiero/api/plugin.py | 26 ++++----- openpype/hosts/houdini/api/plugin.py | 2 +- openpype/hosts/houdini/api/shelves.py | 2 +- .../houdini/plugins/create/convert_legacy.py | 4 +- openpype/hosts/maya/api/commands.py | 2 +- openpype/hosts/maya/api/lib_renderproducts.py | 4 +- .../maya/api/workfile_template_builder.py | 4 +- openpype/hosts/maya/plugins/load/actions.py | 2 +- .../maya/plugins/load/load_arnold_standin.py | 2 +- .../publish/collect_multiverse_look.py | 2 +- .../maya/plugins/publish/extract_look.py | 2 +- .../publish/extract_multiverse_usd_over.py | 2 +- .../plugins/publish/reset_xgen_attributes.py | 2 +- .../publish/validate_camera_attributes.py | 2 +- .../plugins/publish/validate_maya_units.py | 4 +- .../publish/validate_mvlook_contents.py | 2 +- .../publish/validate_renderlayer_aovs.py | 4 +- .../validate_transform_naming_suffix.py | 2 +- openpype/hosts/nuke/api/lib.py | 18 +++---- openpype/hosts/nuke/api/plugin.py | 4 +- openpype/hosts/nuke/api/utils.py | 2 +- .../nuke/api/workfile_template_builder.py | 10 ++-- .../nuke/plugins/create/convert_legacy.py | 2 +- .../nuke/plugins/create/create_source.py | 2 +- .../nuke/plugins/publish/collect_writes.py | 2 +- .../nuke/plugins/publish/validate_backdrop.py | 2 +- .../photoshop/api/extension/host/index.jsx | 6 +-- openpype/hosts/resolve/api/lib.py | 6 +-- openpype/hosts/resolve/api/menu_style.qss | 2 +- openpype/hosts/resolve/api/plugin.py | 16 +++--- .../publish/collect_bulk_mov_instances.py | 4 +- .../plugins/publish/collect_context.py | 4 +- .../plugins/publish/collect_editorial.py | 2 +- .../plugins/publish/validate_frame_ranges.py | 2 +- openpype/hosts/traypublisher/api/editorial.py | 38 ++++++------- .../plugins/create/create_editorial.py | 14 ++--- .../publish/collect_simple_instances.py | 2 +- .../help/validate_layers_visibility.xml | 2 +- .../help/validate_workfile_metadata.xml | 2 +- .../help/validate_workfile_project_name.xml | 2 +- .../plugins/publish/validate_asset_name.py | 2 +- openpype/hosts/unreal/api/pipeline.py | 2 +- .../Source/OpenPype/Private/OpenPypeLib.cpp | 2 +- .../Public/Commandlets/OPActionResult.h | 12 ++--- .../Source/OpenPype/Private/OpenPypeLib.cpp | 2 +- .../Public/Commandlets/OPActionResult.h | 12 ++--- .../hosts/unreal/plugins/load/load_camera.py | 2 +- openpype/hosts/webpublisher/lib.py | 2 +- openpype/lib/applications.py | 2 +- openpype/lib/attribute_definitions.py | 6 +-- openpype/lib/events.py | 2 +- openpype/lib/execute.py | 2 +- openpype/lib/file_transaction.py | 2 +- openpype/lib/transcoding.py | 6 +-- openpype/lib/vendor_bin_utils.py | 4 +- openpype/modules/base.py | 14 ++--- openpype/modules/clockify/clockify_api.py | 2 +- openpype/modules/clockify/clockify_module.py | 2 +- openpype/modules/clockify/widgets.py | 2 +- .../plugins/publish/submit_nuke_deadline.py | 2 +- .../plugins/publish/submit_publish_job.py | 16 +++--- .../action_clone_review_session.py | 2 +- .../action_create_review_session.py | 2 +- .../action_prepare_project.py | 2 +- .../action_tranfer_hierarchical_values.py | 4 +- .../event_next_task_update.py | 2 +- .../event_push_frame_values_to_task.py | 2 +- .../event_radio_buttons.py | 2 +- .../event_sync_to_avalon.py | 14 ++--- .../event_task_to_parent_status.py | 4 +- .../event_user_assigment.py | 4 +- .../event_version_to_task_statuses.py | 2 +- .../action_batch_task_creation.py | 2 +- .../action_create_cust_attrs.py | 6 +-- .../action_create_folders.py | 2 +- .../action_delete_asset.py | 2 +- .../action_delete_old_versions.py | 4 +- .../event_handlers_user/action_delivery.py | 2 +- .../action_fill_workfile_attr.py | 2 +- .../event_handlers_user/action_job_killer.py | 6 +-- .../action_prepare_project.py | 4 +- .../ftrack/event_handlers_user/action_rv.py | 2 +- .../ftrack/event_handlers_user/action_seed.py | 10 ++-- .../action_store_thumbnails_to_avalon.py | 2 +- .../ftrack/ftrack_server/event_server_cli.py | 6 +-- openpype/modules/ftrack/lib/avalon_sync.py | 12 ++--- .../modules/ftrack/lib/custom_attributes.py | 2 +- .../ftrack/lib/ftrack_action_handler.py | 2 +- .../modules/ftrack/lib/ftrack_base_handler.py | 4 +- .../publish/collect_custom_attributes_data.py | 2 +- .../plugins/publish/integrate_ftrack_api.py | 2 +- .../publish/integrate_hierarchy_ftrack.py | 2 +- openpype/modules/ftrack/tray/ftrack_tray.py | 2 +- openpype/modules/interfaces.py | 2 +- openpype/modules/settings_action.py | 6 +-- openpype/pipeline/colorspace.py | 2 +- openpype/plugins/publish/extract_review.py | 2 +- .../schemas/schema_global_publish.json | 2 +- .../publisher/widgets/validations_widget.py | 4 +- 150 files changed, 412 insertions(+), 412 deletions(-) diff --git a/openpype/client/entities.py b/openpype/client/entities.py index c415be8816..7054658c64 100644 --- a/openpype/client/entities.py +++ b/openpype/client/entities.py @@ -3,7 +3,7 @@ Goal is that most of functions here are called on (or with) an object that has project name as a context (e.g. on 'ProjectEntity'?). -+ We will need more specific functions doing wery specific queires really fast. ++ We will need more specific functions doing very specific queries really fast. """ import re @@ -193,7 +193,7 @@ def _get_assets( be found. asset_names (Iterable[str]): Name assets that should be found. parent_ids (Iterable[Union[str, ObjectId]]): Parent asset ids. - standard (bool): Query standart assets (type 'asset'). + standard (bool): Query standard assets (type 'asset'). archived (bool): Query archived assets (type 'archived_asset'). fields (Iterable[str]): Fields that should be returned. All fields are returned if 'None' is passed. @@ -1185,7 +1185,7 @@ def get_representations( standard=True, fields=None ): - """Representaion entities data from one project filtered by filters. + """Representation entities data from one project filtered by filters. Filters are additive (all conditions must pass to return subset). @@ -1231,7 +1231,7 @@ def get_archived_representations( names_by_version_ids=None, fields=None ): - """Archived representaion entities data from project with applied filters. + """Archived representation entities data from project with applied filters. Filters are additive (all conditions must pass to return subset). diff --git a/openpype/client/notes.md b/openpype/client/notes.md index a261b86eca..59743892eb 100644 --- a/openpype/client/notes.md +++ b/openpype/client/notes.md @@ -2,7 +2,7 @@ ## Reason Preparation for OpenPype v4 server. Goal is to remove direct mongo calls in code to prepare a little bit for different source of data for code before. To start think about database calls less as mongo calls but more universally. To do so was implemented simple wrapper around database calls to not use pymongo specific code. -Current goal is not to make universal database model which can be easily replaced with any different source of data but to make it close as possible. Current implementation of OpenPype is too tighly connected to pymongo and it's abilities so we're trying to get closer with long term changes that can be used even in current state. +Current goal is not to make universal database model which can be easily replaced with any different source of data but to make it close as possible. Current implementation of OpenPype is too tightly connected to pymongo and it's abilities so we're trying to get closer with long term changes that can be used even in current state. ## Queries Query functions don't use full potential of mongo queries like very specific queries based on subdictionaries or unknown structures. We try to avoid these calls as much as possible because they'll probably won't be available in future. If it's really necessary a new function can be added but only if it's reasonable for overall logic. All query functions were moved to `~/client/entities.py`. Each function has arguments with available filters and possible reduce of returned keys for each entity. @@ -14,7 +14,7 @@ Changes are a little bit complicated. Mongo has many options how update can happ Create operations expect already prepared document data, for that are prepared functions creating skeletal structures of documents (do not fill all required data), except `_id` all data should be right. Existence of entity is not validated so if the same creation operation is send n times it will create the entity n times which can cause issues. ### Update -Update operation require entity id and keys that should be changed, update dictionary must have {"key": value}. If value should be set in nested dictionary the key must have also all subkeys joined with dot `.` (e.g. `{"data": {"fps": 25}}` -> `{"data.fps": 25}`). To simplify update dictionaries were prepared functions which does that for you, their name has template `prepare__update_data` - they work on comparison of previous document and new document. If there is missing function for requested entity type it is because we didn't need it yet and require implementaion. +Update operation require entity id and keys that should be changed, update dictionary must have {"key": value}. If value should be set in nested dictionary the key must have also all subkeys joined with dot `.` (e.g. `{"data": {"fps": 25}}` -> `{"data.fps": 25}`). To simplify update dictionaries were prepared functions which does that for you, their name has template `prepare__update_data` - they work on comparison of previous document and new document. If there is missing function for requested entity type it is because we didn't need it yet and require implementation. ### Delete Delete operation need entity id. Entity will be deleted from mongo. diff --git a/openpype/client/operations.py b/openpype/client/operations.py index fd639c34a7..ef48f2a1c4 100644 --- a/openpype/client/operations.py +++ b/openpype/client/operations.py @@ -368,7 +368,7 @@ def prepare_workfile_info_update_data(old_doc, new_doc, replace=True): class AbstractOperation(object): """Base operation class. - Opration represent a call into database. The call can create, change or + Operation represent a call into database. The call can create, change or remove data. Args: @@ -409,7 +409,7 @@ class AbstractOperation(object): pass def to_data(self): - """Convert opration to data that can be converted to json or others. + """Convert operation to data that can be converted to json or others. Warning: Current state returns ObjectId objects which cannot be parsed by @@ -428,7 +428,7 @@ class AbstractOperation(object): class CreateOperation(AbstractOperation): - """Opeartion to create an entity. + """Operation to create an entity. Args: project_name (str): On which project operation will happen. @@ -485,7 +485,7 @@ class CreateOperation(AbstractOperation): class UpdateOperation(AbstractOperation): - """Opeartion to update an entity. + """Operation to update an entity. Args: project_name (str): On which project operation will happen. @@ -552,7 +552,7 @@ class UpdateOperation(AbstractOperation): class DeleteOperation(AbstractOperation): - """Opeartion to delete an entity. + """Operation to delete an entity. Args: project_name (str): On which project operation will happen. diff --git a/openpype/host/dirmap.py b/openpype/host/dirmap.py index 1d084cccad..42bf80ecec 100644 --- a/openpype/host/dirmap.py +++ b/openpype/host/dirmap.py @@ -2,7 +2,7 @@ Idea for current dirmap implementation was used from Maya where is possible to enter source and destination roots and maya will try each found source -in referenced file replace with each destionation paths. First path which +in referenced file replace with each destination paths. First path which exists is used. """ @@ -183,7 +183,7 @@ class HostDirmap(object): project_name, remote_site ) # dirmap has sense only with regular disk provider, in the workfile - # wont be root on cloud or sftp provider + # won't be root on cloud or sftp provider if remote_provider != "local_drive": remote_site = "studio" for root_name, active_site_dir in active_overrides.items(): diff --git a/openpype/host/host.py b/openpype/host/host.py index d2335c0062..630fb873a8 100644 --- a/openpype/host/host.py +++ b/openpype/host/host.py @@ -18,7 +18,7 @@ class HostBase(object): Compared to 'avalon' concept: What was before considered as functions in host implementation folder. The host implementation should primarily care about adding ability of creation - (mark subsets to be published) and optionaly about referencing published + (mark subsets to be published) and optionally about referencing published representations as containers. Host may need extend some functionality like working with workfiles @@ -129,9 +129,9 @@ class HostBase(object): """Get current context information. This method should be used to get current context of host. Usage of - this method can be crutial for host implementations in DCCs where + this method can be crucial for host implementations in DCCs where can be opened multiple workfiles at one moment and change of context - can't be catched properly. + can't be caught properly. Default implementation returns values from 'legacy_io.Session'. diff --git a/openpype/host/interfaces.py b/openpype/host/interfaces.py index 999aefd254..39581a3c69 100644 --- a/openpype/host/interfaces.py +++ b/openpype/host/interfaces.py @@ -81,7 +81,7 @@ class ILoadHost: @abstractmethod def get_containers(self): - """Retreive referenced containers from scene. + """Retrieve referenced containers from scene. This can be implemented in hosts where referencing can be used. @@ -191,7 +191,7 @@ class IWorkfileHost: @abstractmethod def get_current_workfile(self): - """Retreive path to current opened file. + """Retrieve path to current opened file. Returns: str: Path to file which is currently opened. @@ -220,7 +220,7 @@ class IWorkfileHost: Default implementation keeps workdir untouched. Warnings: - We must handle this modification with more sofisticated way because + We must handle this modification with more sophisticated way because this can't be called out of DCC so opening of last workfile (calculated before DCC is launched) is complicated. Also breaking defined work template is not a good idea. @@ -302,7 +302,7 @@ class IPublishHost: required methods. Returns: - list[str]: Missing method implementations for new publsher + list[str]: Missing method implementations for new publisher workflow. """ diff --git a/openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx b/openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx index 9b211207de..5c1d163439 100644 --- a/openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx +++ b/openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx @@ -504,7 +504,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 quering if + * found_comp (CompItem, optional): to limit querying if * comp already found previously */ var comp = found_comp || app.project.itemByID(comp_id); diff --git a/openpype/hosts/aftereffects/api/ws_stub.py b/openpype/hosts/aftereffects/api/ws_stub.py index e5d6d9ed89..f094c7fa2a 100644 --- a/openpype/hosts/aftereffects/api/ws_stub.py +++ b/openpype/hosts/aftereffects/api/ws_stub.py @@ -80,7 +80,7 @@ class AfterEffectsServerStub(): Get complete stored JSON with metadata from AE.Metadata.Label field. - It contains containers loaded by any Loader OR instances creted + It contains containers loaded by any Loader OR instances created by Creator. Returns: diff --git a/openpype/hosts/blender/api/ops.py b/openpype/hosts/blender/api/ops.py index 158c32fb5a..91cbfe524f 100644 --- a/openpype/hosts/blender/api/ops.py +++ b/openpype/hosts/blender/api/ops.py @@ -24,7 +24,7 @@ from .workio import OpenFileCacher PREVIEW_COLLECTIONS: Dict = dict() # This seems like a good value to keep the Qt app responsive and doesn't slow -# down Blender. At least on macOS I the interace of Blender gets very laggy if +# down Blender. At least on macOS I the interface of Blender gets very laggy if # you make it smaller. TIMER_INTERVAL: float = 0.01 if platform.system() == "Windows" else 0.1 diff --git a/openpype/hosts/blender/plugins/publish/extract_playblast.py b/openpype/hosts/blender/plugins/publish/extract_playblast.py index 8dc2f66c22..5c3a138c3a 100644 --- a/openpype/hosts/blender/plugins/publish/extract_playblast.py +++ b/openpype/hosts/blender/plugins/publish/extract_playblast.py @@ -50,7 +50,7 @@ class ExtractPlayblast(publish.Extractor): # get isolate objects list isolate = instance.data("isolate", None) - # get ouput path + # get output path stagingdir = self.staging_dir(instance) filename = instance.name path = os.path.join(stagingdir, filename) diff --git a/openpype/hosts/flame/api/lib.py b/openpype/hosts/flame/api/lib.py index 6aca5c5ce6..ab713aed84 100644 --- a/openpype/hosts/flame/api/lib.py +++ b/openpype/hosts/flame/api/lib.py @@ -773,7 +773,7 @@ class MediaInfoFile(object): if logger: self.log = logger - # test if `dl_get_media_info` paht exists + # test if `dl_get_media_info` path exists self._validate_media_script_path() # derivate other feed variables @@ -993,7 +993,7 @@ class MediaInfoFile(object): def _validate_media_script_path(self): if not os.path.isfile(self.MEDIA_SCRIPT_PATH): - raise IOError("Media Scirpt does not exist: `{}`".format( + raise IOError("Media Script does not exist: `{}`".format( self.MEDIA_SCRIPT_PATH)) def _generate_media_info_file(self, fpath, feed_ext, feed_dir): diff --git a/openpype/hosts/flame/api/pipeline.py b/openpype/hosts/flame/api/pipeline.py index 3a23389961..d6fbf750ba 100644 --- a/openpype/hosts/flame/api/pipeline.py +++ b/openpype/hosts/flame/api/pipeline.py @@ -38,7 +38,7 @@ def install(): pyblish.register_plugin_path(PUBLISH_PATH) register_loader_plugin_path(LOAD_PATH) register_creator_plugin_path(CREATE_PATH) - log.info("OpenPype Flame plug-ins registred ...") + log.info("OpenPype Flame plug-ins registered ...") # register callback for switching publishable pyblish.register_callback("instanceToggled", on_pyblish_instance_toggled) diff --git a/openpype/hosts/flame/api/plugin.py b/openpype/hosts/flame/api/plugin.py index 983d7486b3..df8c1ac887 100644 --- a/openpype/hosts/flame/api/plugin.py +++ b/openpype/hosts/flame/api/plugin.py @@ -157,7 +157,7 @@ class CreatorWidget(QtWidgets.QDialog): # convert label text to normal capitalized text with spaces label_text = self.camel_case_split(text) - # assign the new text to lable widget + # assign the new text to label widget label = QtWidgets.QLabel(label_text) label.setObjectName("LineLabel") @@ -345,8 +345,8 @@ class PublishableClip: "track": "sequence", } - # parents search patern - parents_search_patern = r"\{([a-z]*?)\}" + # parents search pattern + parents_search_pattern = r"\{([a-z]*?)\}" # default templates for non-ui use rename_default = False @@ -445,7 +445,7 @@ class PublishableClip: return self.current_segment def _populate_segment_default_data(self): - """ Populate default formating data from segment. """ + """ Populate default formatting data from segment. """ self.current_segment_default_data = { "_folder_": "shots", @@ -538,7 +538,7 @@ class PublishableClip: if not self.index_from_segment: self.count_steps *= self.rename_index - hierarchy_formating_data = {} + hierarchy_formatting_data = {} hierarchy_data = deepcopy(self.hierarchy_data) _data = self.current_segment_default_data.copy() if self.ui_inputs: @@ -552,7 +552,7 @@ class PublishableClip: # mark review layer if self.review_track and ( self.review_track not in self.review_track_default): - # if review layer is defined and not the same as defalut + # if review layer is defined and not the same as default self.review_layer = self.review_track # shot num calculate @@ -578,13 +578,13 @@ class PublishableClip: # fill up pythonic expresisons in hierarchy data for k, _v in hierarchy_data.items(): - hierarchy_formating_data[k] = _v["value"].format(**_data) + hierarchy_formatting_data[k] = _v["value"].format(**_data) else: # if no gui mode then just pass default data - hierarchy_formating_data = hierarchy_data + hierarchy_formatting_data = hierarchy_data tag_hierarchy_data = self._solve_tag_hierarchy_data( - hierarchy_formating_data + hierarchy_formatting_data ) tag_hierarchy_data.update({"heroTrack": True}) @@ -615,27 +615,27 @@ class PublishableClip: # in case track name and subset name is the same then add if self.subset_name == self.track_name: _hero_data["subset"] = self.subset - # assing data to return hierarchy data to tag + # assign data to return hierarchy data to tag tag_hierarchy_data = _hero_data break # add data to return data dict self.marker_data.update(tag_hierarchy_data) - def _solve_tag_hierarchy_data(self, hierarchy_formating_data): + def _solve_tag_hierarchy_data(self, hierarchy_formatting_data): """ Solve marker data from hierarchy data and templates. """ # fill up clip name and hierarchy keys - hierarchy_filled = self.hierarchy.format(**hierarchy_formating_data) - clip_name_filled = self.clip_name.format(**hierarchy_formating_data) + hierarchy_filled = self.hierarchy.format(**hierarchy_formatting_data) + clip_name_filled = self.clip_name.format(**hierarchy_formatting_data) # remove shot from hierarchy data: is not needed anymore - hierarchy_formating_data.pop("shot") + hierarchy_formatting_data.pop("shot") return { "newClipName": clip_name_filled, "hierarchy": hierarchy_filled, "parents": self.parents, - "hierarchyData": hierarchy_formating_data, + "hierarchyData": hierarchy_formatting_data, "subset": self.subset, "family": self.subset_family, "families": [self.family] @@ -650,17 +650,17 @@ class PublishableClip: type ) - # first collect formating data to use for formating template - formating_data = {} + # first collect formatting data to use for formatting template + formatting_data = {} for _k, _v in self.hierarchy_data.items(): value = _v["value"].format( **self.current_segment_default_data) - formating_data[_k] = value + formatting_data[_k] = value return { "entity_type": entity_type, "entity_name": template.format( - **formating_data + **formatting_data ) } @@ -668,9 +668,9 @@ class PublishableClip: """ Create parents and return it in list. """ self.parents = [] - patern = re.compile(self.parents_search_patern) + pattern = re.compile(self.parents_search_pattern) - par_split = [(patern.findall(t).pop(), t) + par_split = [(pattern.findall(t).pop(), t) for t in self.hierarchy.split("/")] for type, template in par_split: @@ -902,22 +902,22 @@ class OpenClipSolver(flib.MediaInfoFile): ): return - formating_data = self._update_formating_data( + formatting_data = self._update_formatting_data( layerName=layer_name, layerUID=layer_uid ) name_obj.text = StringTemplate( self.layer_rename_template - ).format(formating_data) + ).format(formatting_data) - def _update_formating_data(self, **kwargs): - """ Updating formating data for layer rename + def _update_formatting_data(self, **kwargs): + """ Updating formatting data for layer rename Attributes: - key=value (optional): will be included to formating data + key=value (optional): will be included to formatting data as {key: value} Returns: - dict: anatomy context data for formating + dict: anatomy context data for formatting """ self.log.debug(">> self.clip_data: {}".format(self.clip_data)) clip_name_obj = self.clip_data.find("name") diff --git a/openpype/hosts/flame/api/scripts/wiretap_com.py b/openpype/hosts/flame/api/scripts/wiretap_com.py index 4825ff4386..a74172c405 100644 --- a/openpype/hosts/flame/api/scripts/wiretap_com.py +++ b/openpype/hosts/flame/api/scripts/wiretap_com.py @@ -203,7 +203,7 @@ class WireTapCom(object): list: all available volumes in server Rises: - AttributeError: unable to get any volumes childs from server + AttributeError: unable to get any volumes children from server """ root = WireTapNodeHandle(self._server, "/volumes") children_num = WireTapInt(0) diff --git a/openpype/hosts/flame/api/utils.py b/openpype/hosts/flame/api/utils.py index fb8bdee42d..80a5c47e89 100644 --- a/openpype/hosts/flame/api/utils.py +++ b/openpype/hosts/flame/api/utils.py @@ -108,7 +108,7 @@ def _sync_utility_scripts(env=None): shutil.copy2(src, dst) except (PermissionError, FileExistsError) as msg: log.warning( - "Not able to coppy to: `{}`, Problem with: `{}`".format( + "Not able to copy to: `{}`, Problem with: `{}`".format( dst, msg ) diff --git a/openpype/hosts/flame/hooks/pre_flame_setup.py b/openpype/hosts/flame/hooks/pre_flame_setup.py index 713daf1031..8034885c47 100644 --- a/openpype/hosts/flame/hooks/pre_flame_setup.py +++ b/openpype/hosts/flame/hooks/pre_flame_setup.py @@ -153,7 +153,7 @@ class FlamePrelaunch(PreLaunchHook): def _add_pythonpath(self): pythonpath = self.launch_context.env.get("PYTHONPATH") - # separate it explicity by `;` that is what we use in settings + # separate it explicitly by `;` that is what we use in settings new_pythonpath = self.flame_pythonpath.split(os.pathsep) new_pythonpath += pythonpath.split(os.pathsep) diff --git a/openpype/hosts/flame/plugins/create/create_shot_clip.py b/openpype/hosts/flame/plugins/create/create_shot_clip.py index 4fb041a4b2..b01354c313 100644 --- a/openpype/hosts/flame/plugins/create/create_shot_clip.py +++ b/openpype/hosts/flame/plugins/create/create_shot_clip.py @@ -209,7 +209,7 @@ class CreateShotClip(opfapi.Creator): "type": "QComboBox", "label": "Subset Name", "target": "ui", - "toolTip": "chose subset name patern, if [ track name ] is selected, name of track layer will be used", # noqa + "toolTip": "chose subset name pattern, if [ track name ] is selected, name of track layer will be used", # noqa "order": 0}, "subsetFamily": { "value": ["plate", "take"], diff --git a/openpype/hosts/flame/plugins/load/load_clip.py b/openpype/hosts/flame/plugins/load/load_clip.py index 25b31c94a3..dfb2d2b6f0 100644 --- a/openpype/hosts/flame/plugins/load/load_clip.py +++ b/openpype/hosts/flame/plugins/load/load_clip.py @@ -61,9 +61,9 @@ class LoadClip(opfapi.ClipLoader): self.layer_rename_template = self.layer_rename_template.replace( "output", "representation") - formating_data = deepcopy(context["representation"]["context"]) + formatting_data = deepcopy(context["representation"]["context"]) clip_name = StringTemplate(self.clip_name_template).format( - formating_data) + formatting_data) # convert colorspace with ocio to flame mapping # in imageio flame section @@ -88,7 +88,7 @@ class LoadClip(opfapi.ClipLoader): "version": "v{:0>3}".format(version_name), "layer_rename_template": self.layer_rename_template, "layer_rename_patterns": self.layer_rename_patterns, - "context_data": formating_data + "context_data": formatting_data } self.log.debug(pformat( loading_context diff --git a/openpype/hosts/flame/plugins/load/load_clip_batch.py b/openpype/hosts/flame/plugins/load/load_clip_batch.py index 86bc0f8f1e..5c5a77f0d0 100644 --- a/openpype/hosts/flame/plugins/load/load_clip_batch.py +++ b/openpype/hosts/flame/plugins/load/load_clip_batch.py @@ -58,11 +58,11 @@ class LoadClipBatch(opfapi.ClipLoader): self.layer_rename_template = self.layer_rename_template.replace( "output", "representation") - formating_data = deepcopy(context["representation"]["context"]) - formating_data["batch"] = self.batch.name.get_value() + formatting_data = deepcopy(context["representation"]["context"]) + formatting_data["batch"] = self.batch.name.get_value() clip_name = StringTemplate(self.clip_name_template).format( - formating_data) + formatting_data) # convert colorspace with ocio to flame mapping # in imageio flame section @@ -88,7 +88,7 @@ class LoadClipBatch(opfapi.ClipLoader): "version": "v{:0>3}".format(version_name), "layer_rename_template": self.layer_rename_template, "layer_rename_patterns": self.layer_rename_patterns, - "context_data": formating_data + "context_data": formatting_data } self.log.debug(pformat( loading_context diff --git a/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py b/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py index 76d48dded2..23fdf5e785 100644 --- a/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py +++ b/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py @@ -203,7 +203,7 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin): self._get_xml_preset_attrs( attributes, split) - # add xml overides resolution to instance data + # add xml overrides resolution to instance data xml_overrides = attributes["xml_overrides"] if xml_overrides.get("width"): attributes.update({ @@ -284,7 +284,7 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin): self.log.debug("__ head: `{}`".format(head)) self.log.debug("__ tail: `{}`".format(tail)) - # HACK: it is here to serve for versions bellow 2021.1 + # HACK: it is here to serve for versions below 2021.1 if not any([head, tail]): retimed_attributes = get_media_range_with_retimes( otio_clip, handle_start, handle_end) diff --git a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py index 5082217db0..a7979ab4d5 100644 --- a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py +++ b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py @@ -227,7 +227,7 @@ class ExtractSubsetResources(publish.Extractor): self.hide_others( exporting_clip, segment_name, s_track_name) - # change name patern + # change name pattern name_patern_xml = ( "__{}.").format( unique_name) @@ -358,7 +358,7 @@ class ExtractSubsetResources(publish.Extractor): representation_data["stagingDir"] = n_stage_dir files = n_files - # add files to represetation but add + # add files to representation but add # imagesequence as list if ( # first check if path in files is not mov extension diff --git a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py index 4d45f67ded..4f3945bb0f 100644 --- a/openpype/hosts/flame/plugins/publish/integrate_batch_group.py +++ b/openpype/hosts/flame/plugins/publish/integrate_batch_group.py @@ -50,7 +50,7 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin): self._load_clip_to_context(instance, bgroup) def _add_nodes_to_batch_with_links(self, instance, task_data, batch_group): - # get write file node properties > OrederDict because order does mater + # get write file node properties > OrederDict because order does matter write_pref_data = self._get_write_prefs(instance, task_data) batch_nodes = [ diff --git a/openpype/hosts/harmony/api/README.md b/openpype/hosts/harmony/api/README.md index b39f900886..12f21f551a 100644 --- a/openpype/hosts/harmony/api/README.md +++ b/openpype/hosts/harmony/api/README.md @@ -432,11 +432,11 @@ copy_files = """function copyFile(srcFilename, dstFilename) import_files = """function %s_import_files() { - var PNGTransparencyMode = 0; // Premultiplied wih Black - var TGATransparencyMode = 0; // Premultiplied wih Black - var SGITransparencyMode = 0; // Premultiplied wih Black + var PNGTransparencyMode = 0; // Premultiplied with Black + var TGATransparencyMode = 0; // Premultiplied with Black + var SGITransparencyMode = 0; // Premultiplied with Black var LayeredPSDTransparencyMode = 1; // Straight - var FlatPSDTransparencyMode = 2; // Premultiplied wih White + var FlatPSDTransparencyMode = 2; // Premultiplied with White function getUniqueColumnName( column_prefix ) { diff --git a/openpype/hosts/harmony/api/TB_sceneOpened.js b/openpype/hosts/harmony/api/TB_sceneOpened.js index e7cd555332..6614f14f9e 100644 --- a/openpype/hosts/harmony/api/TB_sceneOpened.js +++ b/openpype/hosts/harmony/api/TB_sceneOpened.js @@ -142,10 +142,10 @@ function Client() { }; /** - * Process recieved request. This will eval recieved function and produce + * Process received request. This will eval received function and produce * results. * @function - * @param {object} request - recieved request JSON + * @param {object} request - received request JSON * @return {object} result of evaled function. */ self.processRequest = function(request) { @@ -245,7 +245,7 @@ function Client() { var request = JSON.parse(to_parse); var mid = request.message_id; // self.logDebug('[' + mid + '] - Request: ' + '\n' + JSON.stringify(request)); - self.logDebug('[' + mid + '] Recieved.'); + self.logDebug('[' + mid + '] Received.'); request.result = self.processRequest(request); self.logDebug('[' + mid + '] Processing done.'); @@ -286,8 +286,8 @@ function Client() { /** Harmony 21.1 doesn't have QDataStream anymore. This means we aren't able to write bytes into QByteArray so we had - modify how content lenght is sent do the server. - Content lenght is sent as string of 8 char convertible into integer + modify how content length is sent do the server. + Content length is sent as string of 8 char convertible into integer (instead of 0x00000001[4 bytes] > "000000001"[8 bytes]) */ var codec_name = new QByteArray().append("UTF-8"); diff --git a/openpype/hosts/harmony/api/lib.py b/openpype/hosts/harmony/api/lib.py index e1e77bfbee..8048705dc8 100644 --- a/openpype/hosts/harmony/api/lib.py +++ b/openpype/hosts/harmony/api/lib.py @@ -394,7 +394,7 @@ def get_scene_data(): "function": "AvalonHarmony.getSceneData" })["result"] except json.decoder.JSONDecodeError: - # Means no sceen metadata has been made before. + # Means no scene metadata has been made before. return {} except KeyError: # Means no existing scene metadata has been made. @@ -465,7 +465,7 @@ def imprint(node_id, data, remove=False): Example: >>> from openpype.hosts.harmony.api import lib >>> node = "Top/Display" - >>> data = {"str": "someting", "int": 1, "float": 0.32, "bool": True} + >>> data = {"str": "something", "int": 1, "float": 0.32, "bool": True} >>> lib.imprint(layer, data) """ scene_data = get_scene_data() @@ -550,7 +550,7 @@ def save_scene(): method prevents this double request and safely saves the scene. """ - # Need to turn off the backgound watcher else the communication with + # Need to turn off the background watcher else the communication with # the server gets spammed with two requests at the same time. scene_path = send( {"function": "AvalonHarmony.saveScene"})["result"] diff --git a/openpype/hosts/harmony/api/server.py b/openpype/hosts/harmony/api/server.py index ecf339d91b..04048e5c84 100644 --- a/openpype/hosts/harmony/api/server.py +++ b/openpype/hosts/harmony/api/server.py @@ -61,7 +61,7 @@ class Server(threading.Thread): "module": (str), # Module of method. "method" (str), # Name of method in module. "args" (list), # Arguments to pass to method. - "kwargs" (dict), # Keywork arguments to pass to method. + "kwargs" (dict), # Keyword arguments to pass to method. "reply" (bool), # Optional wait for method completion. } """ diff --git a/openpype/hosts/harmony/vendor/OpenHarmony/README.md b/openpype/hosts/harmony/vendor/OpenHarmony/README.md index 7c77fbfcfa..064afca86c 100644 --- a/openpype/hosts/harmony/vendor/OpenHarmony/README.md +++ b/openpype/hosts/harmony/vendor/OpenHarmony/README.md @@ -6,7 +6,7 @@ Ever tried to make a simple script for toonboom Harmony, then got stumped by the Toonboom Harmony is a very powerful software, with hundreds of functions and tools, and it unlocks a great amount of possibilities for animation studios around the globe. And... being the produce of the hard work of a small team forced to prioritise, it can also be a bit rustic at times! -We are users at heart, animators and riggers, who just want to interact with the software as simply as possible. Simplicity is at the heart of the design of openHarmony. But we also are developpers, and we made the library for people like us who can't resist tweaking the software and bend it in all possible ways, and are looking for powerful functions to help them do it. +We are users at heart, animators and riggers, who just want to interact with the software as simply as possible. Simplicity is at the heart of the design of openHarmony. But we also are developers, and we made the library for people like us who can't resist tweaking the software and bend it in all possible ways, and are looking for powerful functions to help them do it. This library's aim is to create a more direct way to interact with Toonboom through scripts, by providing a more intuitive way to access its elements, and help with the cumbersome and repetitive tasks as well as help unlock untapped potential in its many available systems. So we can go from having to do things like this: @@ -78,7 +78,7 @@ All you have to do is call : ```javascript include("openHarmony.js"); ``` -at the beggining of your script. +at the beginning of your script. You can ask your users to download their copy of the library and store it alongside, or bundle it as you wish as long as you include the license file provided on this repository. @@ -129,7 +129,7 @@ Check that the environment variable `LIB_OPENHARMONY_PATH` is set correctly to t ## How to add openHarmony to vscode intellisense for autocompletion Although not fully supported, you can get most of the autocompletion features to work by adding the following lines to a `jsconfig.json` file placed at the root of your working folder. -The paths need to be relative which means the openHarmony source code must be placed directly in your developping environnement. +The paths need to be relative which means the openHarmony source code must be placed directly in your developping environment. For example, if your working folder contains the openHarmony source in a folder called `OpenHarmony` and your working scripts in a folder called `myScripts`, place the `jsconfig.json` file at the root of the folder and add these lines to the file: diff --git a/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony.js b/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony.js index 530c0902c5..ae65d32a2b 100644 --- a/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony.js +++ b/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony.js @@ -4,7 +4,7 @@ // openHarmony Library // // -// Developped by Mathieu Chaptel, Chris Fourney +// Developed by Mathieu Chaptel, Chris Fourney // // // This library is an open source implementation of a Document Object Model @@ -16,7 +16,7 @@ // and by hiding the heavy lifting required by the official API. // // This library is provided as is and is a work in progress. As such, not every -// function has been implemented or is garanteed to work. Feel free to contribute +// function has been implemented or is guaranteed to work. Feel free to contribute // improvements to its official github. If you do make sure you follow the provided // template and naming conventions and document your new methods properly. // @@ -78,7 +78,7 @@ * $.log("hello"); // prints out a message to the MessageLog. * var myPoint = new $.oPoint(0,0,0); // create a new class instance from an openHarmony class. * - * // function members of the $ objects get published to the global scope, which means $ can be ommited + * // function members of the $ objects get published to the global scope, which means $ can be omitted * * log("hello"); * var myPoint = new oPoint(0,0,0); // This is all valid @@ -118,7 +118,7 @@ Object.defineProperty( $, "directory", { /** - * Wether Harmony is run with the interface or simply from command line + * Whether Harmony is run with the interface or simply from command line */ Object.defineProperty( $, "batchMode", { get: function(){ diff --git a/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_actions.js b/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_actions.js index ad1efc91be..a54f74e147 100644 --- a/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_actions.js +++ b/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_actions.js @@ -4,7 +4,7 @@ // openHarmony Library // // -// Developped by Mathieu Chaptel, Chris Fourney +// Developed by Mathieu Chaptel, Chris Fourney // // // This library is an open source implementation of a Document Object Model @@ -16,7 +16,7 @@ // and by hiding the heavy lifting required by the official API. // // This library is provided as is and is a work in progress. As such, not every -// function has been implemented or is garanteed to work. Feel free to contribute +// function has been implemented or is guaranteed to work. Feel free to contribute // improvements to its official github. If you do make sure you follow the provided // template and naming conventions and document your new methods properly. // @@ -67,7 +67,7 @@ * @hideconstructor * @namespace * @example - * // To check wether an action is available, call the synthax: + * // To check whether an action is available, call the synthax: * Action.validate (, ); * * // To launch an action, call the synthax: diff --git a/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_application.js b/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_application.js index 9e9acb766c..5809cee694 100644 --- a/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_application.js +++ b/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_application.js @@ -4,7 +4,7 @@ // openHarmony Library // // -// Developped by Mathieu Chaptel, Chris Fourney +// Developed by Mathieu Chaptel, Chris Fourney // // // This library is an open source implementation of a Document Object Model @@ -16,7 +16,7 @@ // and by hiding the heavy lifting required by the official API. // // This library is provided as is and is a work in progress. As such, not every -// function has been implemented or is garanteed to work. Feel free to contribute +// function has been implemented or is guaranteed to work. Feel free to contribute // improvements to its official github. If you do make sure you follow the provided // template and naming conventions and document your new methods properly. // @@ -409,7 +409,7 @@ $.oApp.prototype.getToolByName = function(toolName){ /** - * returns the list of stencils useable by the specified tool + * returns the list of stencils usable by the specified tool * @param {$.oTool} tool the tool object we want valid stencils for * @return {$.oStencil[]} the list of stencils compatible with the specified tool */ diff --git a/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_attribute.js b/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_attribute.js index d4d2d791ae..fa044d5b74 100644 --- a/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_attribute.js +++ b/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_attribute.js @@ -4,7 +4,7 @@ // openHarmony Library v0.01 // // -// Developped by Mathieu Chaptel, Chris Fourney... +// Developed by Mathieu Chaptel, Chris Fourney... // // // This library is an open source implementation of a Document Object Model @@ -16,7 +16,7 @@ // and by hiding the heavy lifting required by the official API. // // This library is provided as is and is a work in progress. As such, not every -// function has been implemented or is garanteed to work. Feel free to contribute +// function has been implemented or is guaranteed to work. Feel free to contribute // improvements to its official github. If you do make sure you follow the provided // template and naming conventions and document your new methods properly. // @@ -338,7 +338,7 @@ Object.defineProperty($.oAttribute.prototype, "useSeparate", { * Returns the default value of the attribute for most keywords * @name $.oAttribute#defaultValue * @type {bool} - * @todo switch the implentation to types? + * @todo switch the implementation to types? * @example * // to reset an attribute to its default value: * // (mostly used for position/angle/skew parameters of pegs and drawing nodes) @@ -449,7 +449,7 @@ $.oAttribute.prototype.getLinkedColumns = function(){ /** * Recursively sets an attribute to the same value as another. Both must have the same keyword. - * @param {bool} [duplicateColumns=false] In the case that the attribute has a column, wether to duplicate the column before linking + * @param {bool} [duplicateColumns=false] In the case that the attribute has a column, whether to duplicate the column before linking * @private */ $.oAttribute.prototype.setToAttributeValue = function(attributeToCopy, duplicateColumns){ diff --git a/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_backdrop.js b/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_backdrop.js index c98e194539..1d359f93c4 100644 --- a/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_backdrop.js +++ b/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_backdrop.js @@ -4,7 +4,7 @@ // openHarmony Library // // -// Developped by Mathieu Chaptel, Chris Fourney +// Developed by Mathieu Chaptel, Chris Fourney // // // This library is an open source implementation of a Document Object Model @@ -16,7 +16,7 @@ // and by hiding the heavy lifting required by the official API. // // This library is provided as is and is a work in progress. As such, not every -// function has been implemented or is garanteed to work. Feel free to contribute +// function has been implemented or is guaranteed to work. Feel free to contribute // improvements to its official github. If you do make sure you follow the provided // template and naming conventions and document your new methods properly. // diff --git a/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_color.js b/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_color.js index 7726be6cd6..ff06688e66 100644 --- a/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_color.js +++ b/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_color.js @@ -4,7 +4,7 @@ // openHarmony Library // // -// Developped by Mathieu Chaptel, Chris Fourney +// Developed by Mathieu Chaptel, Chris Fourney // // // This library is an open source implementation of a Document Object Model @@ -16,7 +16,7 @@ // and by hiding the heavy lifting required by the official API. // // This library is provided as is and is a work in progress. As such, not every -// function has been implemented or is garanteed to work. Feel free to contribute +// function has been implemented or is guaranteed to work. Feel free to contribute // improvements to its official github. If you do make sure you follow the provided // template and naming conventions and document your new methods properly. // @@ -158,7 +158,7 @@ $.oColorValue.prototype.fromColorString = function (hexString){ /** - * Uses a color integer (used in backdrops) and parses the INT; applies the RGBA components of the INT to thos oColorValue + * Uses a color integer (used in backdrops) and parses the INT; applies the RGBA components of the INT to the oColorValue * @param { int } colorInt 24 bit-shifted integer containing RGBA values */ $.oColorValue.prototype.parseColorFromInt = function(colorInt){ diff --git a/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_column.js b/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_column.js index 1b73c7943e..f73309049e 100644 --- a/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_column.js +++ b/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_column.js @@ -4,7 +4,7 @@ // openHarmony Library // // -// Developped by Mathieu Chaptel, Chris Fourney +// Developed by Mathieu Chaptel, Chris Fourney // // // This library is an open source implementation of a Document Object Model @@ -16,7 +16,7 @@ // and by hiding the heavy lifting required by the official API. // // This library is provided as is and is a work in progress. As such, not every -// function has been implemented or is garanteed to work. Feel free to contribute +// function has been implemented or is guaranteed to work. Feel free to contribute // improvements to its official github. If you do make sure you follow the provided // template and naming conventions and document your new methods properly. // diff --git a/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_database.js b/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_database.js index 73964c5c38..5440b92875 100644 --- a/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_database.js +++ b/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_database.js @@ -4,7 +4,7 @@ // openHarmony Library // // -// Developped by Mathieu Chaptel, Chris Fourney +// Developed by Mathieu Chaptel, Chris Fourney // // // This library is an open source implementation of a Document Object Model @@ -16,7 +16,7 @@ // and by hiding the heavy lifting required by the official API. // // This library is provided as is and is a work in progress. As such, not every -// function has been implemented or is garanteed to work. Feel free to contribute +// function has been implemented or is guaranteed to work. Feel free to contribute // improvements to its official github. If you do make sure you follow the provided // template and naming conventions and document your new methods properly. // diff --git a/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_dialog.js b/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_dialog.js index a6e16ecb78..3ab78b87d6 100644 --- a/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_dialog.js +++ b/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_dialog.js @@ -5,7 +5,7 @@ // openHarmony Library // // -// Developped by Mathieu Chaptel, Chris Fourney +// Developed by Mathieu Chaptel, Chris Fourney // // // This library is an open source implementation of a Document Object Model @@ -17,7 +17,7 @@ // and by hiding the heavy lifting required by the official API. // // This library is provided as is and is a work in progress. As such, not every -// function has been implemented or is garanteed to work. Feel free to contribute +// function has been implemented or is guaranteed to work. Feel free to contribute // improvements to its official github. If you do make sure you follow the provided // template and naming conventions and document your new methods properly. // @@ -250,7 +250,7 @@ $.oDialog.prototype.prompt = function( labelText, title, prefilledText){ /** * Prompts with a file selector window * @param {string} [text="Select a file:"] The title of the confirmation dialog. - * @param {string} [filter="*"] The filter for the file type and/or file name that can be selected. Accepts wildcard charater "*". + * @param {string} [filter="*"] The filter for the file type and/or file name that can be selected. Accepts wildcard character "*". * @param {string} [getExisting=true] Whether to select an existing file or a save location * @param {string} [acceptMultiple=false] Whether or not selecting more than one file is ok. Is ignored if getExisting is falses. * @param {string} [startDirectory] The directory showed at the opening of the dialog. @@ -327,14 +327,14 @@ $.oDialog.prototype.browseForFolder = function(text, startDirectory){ * @constructor * @classdesc An simple progress dialog to display the progress of a task. * To react to the user clicking the cancel button, connect a function to $.oProgressDialog.canceled() signal. - * When $.batchmode is true, the progress will be outputed as a "Progress : value/range" string to the Harmony stdout. + * When $.batchmode is true, the progress will be outputted as a "Progress : value/range" string to the Harmony stdout. * @param {string} [labelText] The text displayed above the progress bar. * @param {string} [range=100] The maximum value that represents a full progress bar. * @param {string} [title] The title of the dialog * @param {bool} [show=false] Whether to immediately show the dialog. * * @property {bool} wasCanceled Whether the progress bar was cancelled. - * @property {$.oSignal} canceled A Signal emited when the dialog is canceled. Can be connected to a callback. + * @property {$.oSignal} canceled A Signal emitted when the dialog is canceled. Can be connected to a callback. */ $.oProgressDialog = function( labelText, range, title, show ){ if (typeof title === 'undefined') var title = "Progress"; @@ -608,7 +608,7 @@ $.oPieMenu = function( name, widgets, show, minAngle, maxAngle, radius, position this.maxAngle = maxAngle; this.globalCenter = position; - // how wide outisde the icons is the slice drawn + // how wide outside the icons is the slice drawn this._circleMargin = 30; // set these values before calling show() to customize the menu appearance @@ -974,7 +974,7 @@ $.oPieMenu.prototype.getMenuRadius = function(){ var _minRadius = UiLoader.dpiScale(30); var _speed = 10; // the higher the value, the slower the progression - // hyperbolic tangent function to determin the radius + // hyperbolic tangent function to determine the radius var exp = Math.exp(2*itemsNumber/_speed); var _radius = ((exp-1)/(exp+1))*_maxRadius+_minRadius; @@ -1383,7 +1383,7 @@ $.oActionButton.prototype.activate = function(){ * This class is a subclass of QPushButton and all the methods from that class are available to modify this button. * @param {string} paletteName The name of the palette that contains the color * @param {string} colorName The name of the color (if more than one is present, will pick the first match) - * @param {bool} showName Wether to display the name of the color on the button + * @param {bool} showName Whether to display the name of the color on the button * @param {QWidget} parent The parent QWidget for the button. Automatically set during initialisation of the menu. * */ @@ -1437,7 +1437,7 @@ $.oColorButton.prototype.activate = function(){ * @name $.oScriptButton * @constructor * @classdescription This subclass of QPushButton provides an easy way to create a button for a widget that will launch a function from another script file.
    - * The buttons created this way automatically load the icon named after the script if it finds one named like the funtion in a script-icons folder next to the script file.
    + * The buttons created this way automatically load the icon named after the script if it finds one named like the function in a script-icons folder next to the script file.
    * It will also automatically set the callback to lanch the function from the script.
    * This class is a subclass of QPushButton and all the methods from that class are available to modify this button. * @param {string} scriptFile The path to the script file that will be launched diff --git a/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_drawing.js b/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_drawing.js index bad735f237..6f2bc19c0c 100644 --- a/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_drawing.js +++ b/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_drawing.js @@ -4,7 +4,7 @@ // openHarmony Library // // -// Developped by Mathieu Chaptel, Chris Fourney +// Developed by Mathieu Chaptel, Chris Fourney // // // This library is an open source implementation of a Document Object Model @@ -16,7 +16,7 @@ // and by hiding the heavy lifting required by the official API. // // This library is provided as is and is a work in progress. As such, not every -// function has been implemented or is garanteed to work. Feel free to contribute +// function has been implemented or is guaranteed to work. Feel free to contribute // improvements to its official github. If you do make sure you follow the provided // template and naming conventions and document your new methods properly. // @@ -426,7 +426,7 @@ Object.defineProperty($.oDrawing.prototype, 'drawingData', { /** * Import a given file into an existing drawing. * @param {$.oFile} file The path to the file - * @param {bool} [convertToTvg=false] Wether to convert the bitmap to the tvg format (this doesn't vectorise the drawing) + * @param {bool} [convertToTvg=false] Whether to convert the bitmap to the tvg format (this doesn't vectorise the drawing) * * @return { $.oFile } the oFile object pointing to the drawing file after being it has been imported into the element folder. */ @@ -878,8 +878,8 @@ $.oArtLayer.prototype.drawCircle = function(center, radius, lineStyle, fillStyle * @param {$.oVertex[]} path an array of $.oVertex objects that describe a path. * @param {$.oLineStyle} [lineStyle] the line style to draw with. (By default, will use the current stencil selection) * @param {$.oFillStyle} [fillStyle] the fill information for the path. (By default, will use the current palette selection) - * @param {bool} [polygon] Wether bezier handles should be created for the points in the path (ignores "onCurve" properties of oVertex from path) - * @param {bool} [createUnderneath] Wether the new shape will appear on top or underneath the contents of the layer. (not working yet) + * @param {bool} [polygon] Whether bezier handles should be created for the points in the path (ignores "onCurve" properties of oVertex from path) + * @param {bool} [createUnderneath] Whether the new shape will appear on top or underneath the contents of the layer. (not working yet) */ $.oArtLayer.prototype.drawShape = function(path, lineStyle, fillStyle, polygon, createUnderneath){ if (typeof fillStyle === 'undefined') var fillStyle = new this.$.oFillStyle(); @@ -959,7 +959,7 @@ $.oArtLayer.prototype.drawContour = function(path, fillStyle){ * @param {float} width the width of the rectangle. * @param {float} height the height of the rectangle. * @param {$.oLineStyle} lineStyle a line style to use for the rectangle stroke. - * @param {$.oFillStyle} fillStyle a fill style to use for the rectange fill. + * @param {$.oFillStyle} fillStyle a fill style to use for the rectangle fill. * @returns {$.oShape} the shape containing the added stroke. */ $.oArtLayer.prototype.drawRectangle = function(x, y, width, height, lineStyle, fillStyle){ @@ -1514,7 +1514,7 @@ Object.defineProperty($.oStroke.prototype, "path", { /** - * The oVertex that are on the stroke (Bezier handles exluded.) + * The oVertex that are on the stroke (Bezier handles excluded.) * The first is repeated at the last position when the stroke is closed. * @name $.oStroke#points * @type {$.oVertex[]} @@ -1583,7 +1583,7 @@ Object.defineProperty($.oStroke.prototype, "style", { /** - * wether the stroke is a closed shape. + * whether the stroke is a closed shape. * @name $.oStroke#closed * @type {bool} */ @@ -1919,7 +1919,7 @@ $.oContour.prototype.toString = function(){ * @constructor * @classdesc * The $.oVertex class represents a single control point on a stroke. This class is used to get the index of the point in the stroke path sequence, as well as its position as a float along the stroke's length. - * The onCurve property describes wether this control point is a bezier handle or a point on the curve. + * The onCurve property describes whether this control point is a bezier handle or a point on the curve. * * @param {$.oStroke} stroke the stroke that this vertex belongs to * @param {float} x the x coordinate of the vertex, in drawing space diff --git a/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_element.js b/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_element.js index ed50d6e50b..b64c8169ec 100644 --- a/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_element.js +++ b/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_element.js @@ -4,7 +4,7 @@ // openHarmony Library v0.01 // // -// Developped by Mathieu Chaptel, Chris Fourney... +// Developed by Mathieu Chaptel, Chris Fourney... // // // This library is an open source implementation of a Document Object Model @@ -16,7 +16,7 @@ // and by hiding the heavy lifting required by the official API. // // This library is provided as is and is a work in progress. As such, not every -// function has been implemented or is garanteed to work. Feel free to contribute +// function has been implemented or is guaranteed to work. Feel free to contribute // improvements to its official github. If you do make sure you follow the provided // template and naming conventions and document your new methods properly. // diff --git a/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_file.js b/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_file.js index 14dafa3b63..50e4b0d475 100644 --- a/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_file.js +++ b/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_file.js @@ -4,7 +4,7 @@ // openHarmony Library // // -// Developped by Mathieu Chaptel, Chris Fourney +// Developed by Mathieu Chaptel, Chris Fourney // // // This library is an open source implementation of a Document Object Model @@ -16,7 +16,7 @@ // and by hiding the heavy lifting required by the official API. // // This library is provided as is and is a work in progress. As such, not every -// function has been implemented or is garanteed to work. Feel free to contribute +// function has been implemented or is guaranteed to work. Feel free to contribute // improvements to its official github. If you do make sure you follow the provided // template and naming conventions and document your new methods properly. // @@ -509,7 +509,7 @@ Object.defineProperty($.oFile.prototype, 'fullName', { /** - * The name of the file without extenstion. + * The name of the file without extension. * @name $.oFile#name * @type {string} */ diff --git a/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_frame.js b/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_frame.js index 37bdede02a..e1d1dd7fad 100644 --- a/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_frame.js +++ b/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_frame.js @@ -4,7 +4,7 @@ // openHarmony Library // // -// Developped by Mathieu Chaptel, Chris Fourney +// Developed by Mathieu Chaptel, Chris Fourney // // // This library is an open source implementation of a Document Object Model @@ -16,7 +16,7 @@ // and by hiding the heavy lifting required by the official API. // // This library is provided as is and is a work in progress. As such, not every -// function has been implemented or is garanteed to work. Feel free to contribute +// function has been implemented or is guaranteed to work. Feel free to contribute // improvements to its official github. If you do make sure you follow the provided // template and naming conventions and document your new methods properly. // @@ -263,7 +263,7 @@ Object.defineProperty($.oFrame.prototype, 'duration', { return _sceneLength; } - // walk up the frames of the scene to the next keyFrame to determin duration + // walk up the frames of the scene to the next keyFrame to determine duration var _frames = this.column.frames for (var i=this.frameNumber+1; i<_sceneLength; i++){ if (_frames[i].isKeyframe) return _frames[i].frameNumber - _startFrame; @@ -426,7 +426,7 @@ Object.defineProperty($.oFrame.prototype, 'velocity', { * easeIn : a $.oPoint object representing the left handle for bezier columns, or a {point, ease} object for ease columns. * easeOut : a $.oPoint object representing the left handle for bezier columns, or a {point, ease} object for ease columns. * continuity : the type of bezier used by the point. - * constant : wether the frame is interpolated or a held value. + * constant : whether the frame is interpolated or a held value. * @name $.oFrame#ease * @type {oPoint/object} */ @@ -520,7 +520,7 @@ Object.defineProperty($.oFrame.prototype, 'easeOut', { /** - * Determines the frame's continuity setting. Can take the values "CORNER", (two independant bezier handles on each side), "SMOOTH"(handles are aligned) or "STRAIGHT" (no handles and in straight lines). + * Determines the frame's continuity setting. Can take the values "CORNER", (two independent bezier handles on each side), "SMOOTH"(handles are aligned) or "STRAIGHT" (no handles and in straight lines). * @name $.oFrame#continuity * @type {string} */ diff --git a/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_list.js b/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_list.js index 9d02b1c2aa..63a5c0eeb8 100644 --- a/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_list.js +++ b/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_list.js @@ -4,7 +4,7 @@ // openHarmony Library v0.01 // // -// Developped by Mathieu Chaptel, Chris Fourney... +// Developed by Mathieu Chaptel, Chris Fourney... // // // This library is an open source implementation of a Document Object Model @@ -16,7 +16,7 @@ // and by hiding the heavy lifting required by the official API. // // This library is provided as is and is a work in progress. As such, not every -// function has been implemented or is garanteed to work. Feel free to contribute +// function has been implemented or is guaranteed to work. Feel free to contribute // improvements to its official github. If you do make sure you follow the provided // template and naming conventions and document your new methods properly. // @@ -516,5 +516,5 @@ Object.defineProperty($.oList.prototype, 'toString', { -//Needs all filtering, limiting. mapping, pop, concat, join, ect +//Needs all filtering, limiting. mapping, pop, concat, join, etc //Speed up by finessing the way it extends and tracks the enumerable properties. \ No newline at end of file diff --git a/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_math.js b/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_math.js index c0d4ca99a7..06bfb51f30 100644 --- a/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_math.js +++ b/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_math.js @@ -4,7 +4,7 @@ // openHarmony Library // // -// Developped by Mathieu Chaptel, Chris Fourney +// Developed by Mathieu Chaptel, Chris Fourney // // // This library is an open source implementation of a Document Object Model @@ -16,7 +16,7 @@ // and by hiding the heavy lifting required by the official API. // // This library is provided as is and is a work in progress. As such, not every -// function has been implemented or is garanteed to work. Feel free to contribute +// function has been implemented or is guaranteed to work. Feel free to contribute // improvements to its official github. If you do make sure you follow the provided // template and naming conventions and document your new methods properly. // @@ -193,7 +193,7 @@ $.oPoint.prototype.pointSubtract = function( sub_pt ){ /** * Subtracts the point to the coordinates of the current oPoint and returns a new oPoint with the result. * @param {$.oPoint} point The point to subtract to this point. - * @returns {$.oPoint} a new independant oPoint. + * @returns {$.oPoint} a new independent oPoint. */ $.oPoint.prototype.subtractPoint = function( point ){ var x = this.x - point.x; @@ -298,9 +298,9 @@ $.oPoint.prototype.convertToWorldspace = function(){ /** - * Linearily Interpolate between this (0.0) and the provided point (1.0) + * Linearly Interpolate between this (0.0) and the provided point (1.0) * @param {$.oPoint} point The target point at 100% - * @param {double} perc 0-1.0 value to linearily interp + * @param {double} perc 0-1.0 value to linearly interp * * @return: { $.oPoint } The interpolated value. */ @@ -410,9 +410,9 @@ $.oBox.prototype.include = function(box){ /** - * Checks wether the box contains another $.oBox. + * Checks whether the box contains another $.oBox. * @param {$.oBox} box The $.oBox to check for. - * @param {bool} [partial=false] wether to accept partially contained boxes. + * @param {bool} [partial=false] whether to accept partially contained boxes. */ $.oBox.prototype.contains = function(box, partial){ if (typeof partial === 'undefined') var partial = false; @@ -537,7 +537,7 @@ $.oMatrix.prototype.toString = function(){ * @classdesc The $.oVector is a replacement for the Vector3d objects of Harmony. * @param {float} x a x coordinate for this vector. * @param {float} y a y coordinate for this vector. - * @param {float} [z=0] a z coordinate for this vector. If ommited, will be set to 0 and vector will be 2D. + * @param {float} [z=0] a z coordinate for this vector. If omitted, will be set to 0 and vector will be 2D. */ $.oVector = function(x, y, z){ if (typeof z === "undefined" || isNaN(z)) var z = 0; diff --git a/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_metadata.js b/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_metadata.js index c19e6d12f4..29afeb522c 100644 --- a/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_metadata.js +++ b/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_metadata.js @@ -4,7 +4,7 @@ // openHarmony Library v0.01 // // -// Developped by Mathieu Chaptel, Chris Fourney... +// Developed by Mathieu Chaptel, Chris Fourney... // // // This library is an open source implementation of a Document Object Model @@ -16,7 +16,7 @@ // and by hiding the heavy lifting required by the official API. // // This library is provided as is and is a work in progress. As such, not every -// function has been implemented or is garanteed to work. Feel free to contribute +// function has been implemented or is guaranteed to work. Feel free to contribute // improvements to its official github. If you do make sure you follow the provided // template and naming conventions and document your new methods properly. // diff --git a/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_misc.js b/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_misc.js index fec5d32816..6ef75f5560 100644 --- a/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_misc.js +++ b/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_misc.js @@ -4,7 +4,7 @@ // openHarmony Library v0.01 // // -// Developped by Mathieu Chaptel, Chris Fourney... +// Developed by Mathieu Chaptel, Chris Fourney... // // // This library is an open source implementation of a Document Object Model @@ -16,7 +16,7 @@ // and by hiding the heavy lifting required by the official API. // // This library is provided as is and is a work in progress. As such, not every -// function has been implemented or is garanteed to work. Feel free to contribute +// function has been implemented or is guaranteed to work. Feel free to contribute // improvements to its official github. If you do make sure you follow the provided // template and naming conventions and document your new methods properly. // @@ -54,7 +54,7 @@ /** - * The $.oUtils helper class -- providing generic utilities. Doesn't need instanciation. + * The $.oUtils helper class -- providing generic utilities. Doesn't need instantiation. * @classdesc $.oUtils utility Class */ $.oUtils = function(){ diff --git a/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_network.js b/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_network.js index a4476d7591..2a6aa3519a 100644 --- a/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_network.js +++ b/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_network.js @@ -4,7 +4,7 @@ // openHarmony Library v0.01 // // -// Developped by Mathieu Chaptel, Chris Fourney... +// Developed by Mathieu Chaptel, Chris Fourney... // // // This library is an open source implementation of a Document Object Model @@ -16,7 +16,7 @@ // and by hiding the heavy lifting required by the official API. // // This library is provided as is and is a work in progress. As such, not every -// function has been implemented or is garanteed to work. Feel free to contribute +// function has been implemented or is guaranteed to work. Feel free to contribute // improvements to its official github. If you do make sure you follow the provided // template and naming conventions and document your new methods properly. // @@ -87,7 +87,7 @@ $.oNetwork = function( ){ * @param {function} callback_func Providing a callback function prevents blocking, and will respond on this function. The callback function is in form func( results ){} * @param {bool} use_json In the event of a JSON api, this will return an object converted from the returned JSON. * - * @return: {string/object} The resulting object/string from the query -- otherwise a bool as false when an error occured.. + * @return: {string/object} The resulting object/string from the query -- otherwise a bool as false when an error occurred.. */ $.oNetwork.prototype.webQuery = function ( address, callback_func, use_json ){ if (typeof callback_func === 'undefined') var callback_func = false; @@ -272,7 +272,7 @@ $.oNetwork.prototype.webQuery = function ( address, callback_func, use_json ){ * @param {function} path The local file path to save the download. * @param {bool} replace Replace the file if it exists. * - * @return: {string/object} The resulting object/string from the query -- otherwise a bool as false when an error occured.. + * @return: {string/object} The resulting object/string from the query -- otherwise a bool as false when an error occurred.. */ $.oNetwork.prototype.downloadSingle = function ( address, path, replace ){ if (typeof replace === 'undefined') var replace = false; diff --git a/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_node.js b/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_node.js index 5590d7b7e9..deb1854357 100644 --- a/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_node.js +++ b/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_node.js @@ -4,7 +4,7 @@ // openHarmony Library // // -// Developped by Mathieu Chaptel, Chris Fourney +// Developed by Mathieu Chaptel, Chris Fourney // // // This library is an open source implementation of a Document Object Model @@ -16,7 +16,7 @@ // and by hiding the heavy lifting required by the official API. // // This library is provided as is and is a work in progress. As such, not every -// function has been implemented or is garanteed to work. Feel free to contribute +// function has been implemented or is guaranteed to work. Feel free to contribute // improvements to its official github. If you do make sure you follow the provided // template and naming conventions and document your new methods properly. // @@ -562,7 +562,7 @@ Object.defineProperty($.oNode.prototype, 'height', { /** - * The list of oNodeLinks objects descibing the connections to the inport of this node, in order of inport. + * The list of oNodeLinks objects describing the connections to the inport of this node, in order of inport. * @name $.oNode#inLinks * @readonly * @deprecated returns $.oNodeLink instances but $.oLink is preferred. Use oNode.getInLinks() instead. @@ -658,7 +658,7 @@ Object.defineProperty($.oNode.prototype, 'outPorts', { /** - * The list of oNodeLinks objects descibing the connections to the outports of this node, in order of outport. + * The list of oNodeLinks objects describing the connections to the outports of this node, in order of outport. * @name $.oNode#outLinks * @readonly * @type {$.oNodeLink[]} @@ -1666,7 +1666,7 @@ $.oNode.prototype.refreshAttributes = function( ){ * It represents peg nodes in the scene. * @constructor * @augments $.oNode - * @classdesc Peg Moudle Class + * @classdesc Peg Module Class * @param {string} path Path to the node in the network. * @param {oScene} oSceneObject Access to the oScene object of the DOM. */ @@ -1886,7 +1886,7 @@ $.oDrawingNode.prototype.getDrawingAtFrame = function(frameNumber){ /** - * Gets the list of palettes containing colors used by a drawing node. This only gets palettes with the first occurence of the colors. + * Gets the list of palettes containing colors used by a drawing node. This only gets palettes with the first occurrence of the colors. * @return {$.oPalette[]} The palettes that contain the color IDs used by the drawings of the node. */ $.oDrawingNode.prototype.getUsedPalettes = function(){ @@ -1968,7 +1968,7 @@ $.oDrawingNode.prototype.unlinkPalette = function(oPaletteObject){ * Duplicates a node by creating an independent copy. * @param {string} [newName] The new name for the duplicated node. * @param {oPoint} [newPosition] The new position for the duplicated node. - * @param {bool} [duplicateElement] Wether to also duplicate the element. + * @param {bool} [duplicateElement] Whether to also duplicate the element. */ $.oDrawingNode.prototype.duplicate = function(newName, newPosition, duplicateElement){ if (typeof newPosition === 'undefined') var newPosition = this.nodePosition; @@ -2464,7 +2464,7 @@ $.oGroupNode.prototype.getNodeByName = function(name){ * Returns all the nodes of a certain type in the group. * Pass a value to recurse to look into the groups as well. * @param {string} typeName The type of the nodes. - * @param {bool} recurse Wether to look inside the groups. + * @param {bool} recurse Whether to look inside the groups. * * @return {$.oNode[]} The nodes found. */ @@ -2626,7 +2626,7 @@ $.oGroupNode.prototype.orderNodeView = function(recurse){ * * peg.linkOutNode(drawingNode); * - * //through all this we didn't specify nodePosition parameters so we'll sort evertything at once + * //through all this we didn't specify nodePosition parameters so we'll sort everything at once * * sceneRoot.orderNodeView(); * @@ -3333,7 +3333,7 @@ $.oGroupNode.prototype.importImageAsTVG = function(path, alignment, nodePosition * imports an image sequence as a node into the current group. * @param {$.oFile[]} imagePaths a list of paths to the images to import (can pass a list of strings or $.oFile) * @param {number} [exposureLength=1] the number of frames each drawing should be exposed at. If set to 0/false, each drawing will use the numbering suffix of the file to set its frame. - * @param {boolean} [convertToTvg=false] wether to convert the files to tvg during import + * @param {boolean} [convertToTvg=false] whether to convert the files to tvg during import * @param {string} [alignment="ASIS"] the alignment to apply to the node * @param {$.oPoint} [nodePosition] the position of the node in the nodeview * @@ -3346,7 +3346,7 @@ $.oGroupNode.prototype.importImageSequence = function(imagePaths, exposureLength if (typeof extendScene === 'undefined') var extendScene = false; - // match anything but capture trailing numbers and separates punctuation preceeding it + // match anything but capture trailing numbers and separates punctuation preceding it var numberingRe = /(.*?)([\W_]+)?(\d*)$/i; // sanitize imagePaths diff --git a/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_nodeLink.js b/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_nodeLink.js index 279a871691..07a4d147da 100644 --- a/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_nodeLink.js +++ b/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony/openHarmony_nodeLink.js @@ -4,7 +4,7 @@ // openHarmony Library v0.01 // // -// Developped by Mathieu Chaptel, Chris Fourney... +// Developed by Mathieu Chaptel, Chris Fourney... // // // This library is an open source implementation of a Document Object Model @@ -16,7 +16,7 @@ // and by hiding the heavy lifting required by the official API. // // This library is provided as is and is a work in progress. As such, not every -// function has been implemented or is garanteed to work. Feel free to contribute +// function has been implemented or is guaranteed to work. Feel free to contribute // improvements to its official github. If you do make sure you follow the provided // template and naming conventions and document your new methods properly. // @@ -174,7 +174,7 @@ Object.defineProperty($.oNodeLink.prototype, 'outNode', { return; } - this.apply(); // do we really want to apply everytime we set? + this.apply(); // do we really want to apply every time we set? } }); @@ -198,7 +198,7 @@ Object.defineProperty($.oNodeLink.prototype, 'inNode', { return; } - this.apply(); // do we really want to apply everytime we set? + this.apply(); // do we really want to apply every time we set? } }); @@ -222,7 +222,7 @@ Object.defineProperty($.oNodeLink.prototype, 'outPort', { return; } - this.apply(); // do we really want to apply everytime we set? + this.apply(); // do we really want to apply every time we set? } }); @@ -256,7 +256,7 @@ Object.defineProperty($.oNodeLink.prototype, 'inPort', { return; } - this.apply(); // do we really want to apply everytime we set? + this.apply(); // do we really want to apply every time we set? } }); @@ -983,7 +983,7 @@ $.oNodeLink.prototype.validate = function ( ) { * @return {bool} Whether the connection is a valid connection that exists currently in the node system. */ $.oNodeLink.prototype.validateUpwards = function( inport, outportProvided ) { - //IN THE EVENT OUTNODE WASNT PROVIDED. + //IN THE EVENT OUTNODE WASN'T PROVIDED. this.path = this.findInputPath( this._inNode, inport, [] ); if( !this.path || this.path.length == 0 ){ return false; @@ -1173,7 +1173,7 @@ Object.defineProperty($.oLink.prototype, 'outPort', { /** - * The index of the link comming out of the out-port. + * The index of the link coming out of the out-port. *
    In the event this value wasn't known by the link object but the link is actually connected, the correct value will be found. * @name $.oLink#outLink * @readonly @@ -1323,7 +1323,7 @@ $.oLink.prototype.getValidLink = function(createOutPorts, createInPorts){ /** - * Attemps to connect a link. Will guess the ports if not provided. + * Attempts to connect a link. Will guess the ports if not provided. * @return {bool} */ $.oLink.prototype.connect = function(){ @@ -1623,11 +1623,11 @@ $.oLinkPath.prototype.findExistingPath = function(){ /** - * Gets a link object from two nodes that can be succesfully connected. Provide port numbers if there are specific requirements to match. If a link already exists, it will be returned. + * Gets a link object from two nodes that can be successfully connected. Provide port numbers if there are specific requirements to match. If a link already exists, it will be returned. * @param {$.oNode} start The node from which the link originates. * @param {$.oNode} end The node at which the link ends. - * @param {int} [outPort] A prefered out-port for the link to use. - * @param {int} [inPort] A prefered in-port for the link to use. + * @param {int} [outPort] A preferred out-port for the link to use. + * @param {int} [inPort] A preferred in-port for the link to use. * * @return {$.oLink} the valid $.oLink object. Returns null if no such link could be created (for example if the node's in-port is already linked) */ diff --git a/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony_tools.js b/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony_tools.js index 57d4a63e96..9014929fc4 100644 --- a/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony_tools.js +++ b/openpype/hosts/harmony/vendor/OpenHarmony/openHarmony_tools.js @@ -4,7 +4,7 @@ // openHarmony Library v0.01 // // -// Developped by Mathieu Chaptel, ... +// Developed by Mathieu Chaptel, ... // // // This library is an open source implementation of a Document Object Model @@ -16,7 +16,7 @@ // and by hiding the heavy lifting required by the official API. // // This library is provided as is and is a work in progress. As such, not every -// function has been implemented or is garanteed to work. Feel free to contribute +// function has been implemented or is guaranteed to work. Feel free to contribute // improvements to its official github. If you do make sure you follow the provided // template and naming conventions and document your new methods properly. // @@ -212,7 +212,7 @@ function openHarmony_toolInstaller(){ //---------------------------------------------- - //-- GET THE FILE CONTENTS IN A DIRCTORY ON GIT + //-- GET THE FILE CONTENTS IN A DIRECTORY ON GIT this.recurse_files = function( contents, arr_files ){ with( context.$.global ){ try{ @@ -501,7 +501,7 @@ function openHarmony_toolInstaller(){ var download_item = item["download_url"]; var query = $.network.webQuery( download_item, false, false ); if( query ){ - //INSTALL TYPES ARE script, package, ect. + //INSTALL TYPES ARE script, package, etc. if( install_types[ m.install_cache[ item["url"] ] ] ){ m.installLabel.text = install_types[ m.install_cache[ item["url"] ] ]; diff --git a/openpype/hosts/harmony/vendor/OpenHarmony/package.json b/openpype/hosts/harmony/vendor/OpenHarmony/package.json index c62ecbc9d8..7a535cdcf6 100644 --- a/openpype/hosts/harmony/vendor/OpenHarmony/package.json +++ b/openpype/hosts/harmony/vendor/OpenHarmony/package.json @@ -1,7 +1,7 @@ { "name": "openharmony", "version": "0.0.1", - "description": "An Open Source Imlementation of a Document Object Model for the Toonboom Harmony scripting interface", + "description": "An Open Source Implementation of a Document Object Model for the Toonboom Harmony scripting interface", "main": "openHarmony.js", "scripts": { "test": "$", diff --git a/openpype/hosts/hiero/api/__init__.py b/openpype/hosts/hiero/api/__init__.py index 1fa40c9f74..b95c0fe1d7 100644 --- a/openpype/hosts/hiero/api/__init__.py +++ b/openpype/hosts/hiero/api/__init__.py @@ -108,7 +108,7 @@ __all__ = [ "apply_colorspace_project", "apply_colorspace_clips", "get_sequence_pattern_and_padding", - # depricated + # deprecated "get_track_item_pype_tag", "set_track_item_pype_tag", "get_track_item_pype_data", diff --git a/openpype/hosts/hiero/api/pipeline.py b/openpype/hosts/hiero/api/pipeline.py index 4ab73e7d19..d88aeac810 100644 --- a/openpype/hosts/hiero/api/pipeline.py +++ b/openpype/hosts/hiero/api/pipeline.py @@ -193,8 +193,8 @@ def parse_container(item, validate=True): return # convert the data to list and validate them for _, obj_data in _data.items(): - cotnainer = data_to_container(item, obj_data) - return_list.append(cotnainer) + container = data_to_container(item, obj_data) + return_list.append(container) return return_list else: _data = lib.get_trackitem_openpype_data(item) diff --git a/openpype/hosts/hiero/api/plugin.py b/openpype/hosts/hiero/api/plugin.py index 5ca901caaa..a3f8a6c524 100644 --- a/openpype/hosts/hiero/api/plugin.py +++ b/openpype/hosts/hiero/api/plugin.py @@ -411,7 +411,7 @@ class ClipLoader: self.with_handles = options.get("handles") or bool( options.get("handles") is True) # try to get value from options or evaluate key value for `load_how` - self.sequencial_load = options.get("sequencially") or bool( + self.sequencial_load = options.get("sequentially") or bool( "Sequentially in order" in options.get("load_how", "")) # try to get value from options or evaluate key value for `load_to` self.new_sequence = options.get("newSequence") or bool( @@ -836,7 +836,7 @@ class PublishClip: # increasing steps by index of rename iteration self.count_steps *= self.rename_index - hierarchy_formating_data = {} + hierarchy_formatting_data = {} hierarchy_data = deepcopy(self.hierarchy_data) _data = self.track_item_default_data.copy() if self.ui_inputs: @@ -871,13 +871,13 @@ class PublishClip: # fill up pythonic expresisons in hierarchy data for k, _v in hierarchy_data.items(): - hierarchy_formating_data[k] = _v["value"].format(**_data) + hierarchy_formatting_data[k] = _v["value"].format(**_data) else: # if no gui mode then just pass default data - hierarchy_formating_data = hierarchy_data + hierarchy_formatting_data = hierarchy_data tag_hierarchy_data = self._solve_tag_hierarchy_data( - hierarchy_formating_data + hierarchy_formatting_data ) tag_hierarchy_data.update({"heroTrack": True}) @@ -905,20 +905,20 @@ class PublishClip: # add data to return data dict self.tag_data.update(tag_hierarchy_data) - def _solve_tag_hierarchy_data(self, hierarchy_formating_data): + def _solve_tag_hierarchy_data(self, hierarchy_formatting_data): """ Solve tag data from hierarchy data and templates. """ # fill up clip name and hierarchy keys - hierarchy_filled = self.hierarchy.format(**hierarchy_formating_data) - clip_name_filled = self.clip_name.format(**hierarchy_formating_data) + hierarchy_filled = self.hierarchy.format(**hierarchy_formatting_data) + clip_name_filled = self.clip_name.format(**hierarchy_formatting_data) # remove shot from hierarchy data: is not needed anymore - hierarchy_formating_data.pop("shot") + hierarchy_formatting_data.pop("shot") return { "newClipName": clip_name_filled, "hierarchy": hierarchy_filled, "parents": self.parents, - "hierarchyData": hierarchy_formating_data, + "hierarchyData": hierarchy_formatting_data, "subset": self.subset, "family": self.subset_family, "families": [self.data["family"]] @@ -934,16 +934,16 @@ class PublishClip: ) # first collect formatting data to use for formatting template - formating_data = {} + formatting_data = {} for _k, _v in self.hierarchy_data.items(): value = _v["value"].format( **self.track_item_default_data) - formating_data[_k] = value + formatting_data[_k] = value return { "entity_type": entity_type, "entity_name": template.format( - **formating_data + **formatting_data ) } diff --git a/openpype/hosts/houdini/api/plugin.py b/openpype/hosts/houdini/api/plugin.py index f0985973a6..340a7f0770 100644 --- a/openpype/hosts/houdini/api/plugin.py +++ b/openpype/hosts/houdini/api/plugin.py @@ -60,7 +60,7 @@ class Creator(LegacyCreator): def process(self): instance = super(CreateEpicNode, self, process() - # Set paramaters for Alembic node + # Set parameters for Alembic node instance.setParms( {"sop_path": "$HIP/%s.abc" % self.nodes[0]} ) diff --git a/openpype/hosts/houdini/api/shelves.py b/openpype/hosts/houdini/api/shelves.py index ebd668e9e4..6e0f367f62 100644 --- a/openpype/hosts/houdini/api/shelves.py +++ b/openpype/hosts/houdini/api/shelves.py @@ -69,7 +69,7 @@ def generate_shelves(): mandatory_attributes = {'label', 'script'} for tool_definition in shelf_definition.get('tools_list'): - # We verify that the name and script attibutes of the tool + # We verify that the name and script attributes of the tool # are set if not all( tool_definition[key] for key in mandatory_attributes diff --git a/openpype/hosts/houdini/plugins/create/convert_legacy.py b/openpype/hosts/houdini/plugins/create/convert_legacy.py index 4b8041b4f5..e549c9dc26 100644 --- a/openpype/hosts/houdini/plugins/create/convert_legacy.py +++ b/openpype/hosts/houdini/plugins/create/convert_legacy.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""Convertor for legacy Houdini subsets.""" +"""Converter for legacy Houdini subsets.""" from openpype.pipeline.create.creator_plugins import SubsetConvertorPlugin from openpype.hosts.houdini.api.lib import imprint @@ -7,7 +7,7 @@ from openpype.hosts.houdini.api.lib import imprint class HoudiniLegacyConvertor(SubsetConvertorPlugin): """Find and convert any legacy subsets in the scene. - This Convertor will find all legacy subsets in the scene and will + This Converter will find all legacy subsets in the scene and will transform them to the current system. Since the old subsets doesn't retain any information about their original creators, the only mapping we can do is based on their families. diff --git a/openpype/hosts/maya/api/commands.py b/openpype/hosts/maya/api/commands.py index 018340d86c..3e31875fd8 100644 --- a/openpype/hosts/maya/api/commands.py +++ b/openpype/hosts/maya/api/commands.py @@ -69,7 +69,7 @@ def _resolution_from_document(doc): resolution_width = doc["data"].get("resolution_width") resolution_height = doc["data"].get("resolution_height") - # Make sure both width and heigh are set + # Make sure both width and height are set if resolution_width is None or resolution_height is None: cmds.warning( "No resolution information found for \"{}\"".format(doc["name"]) diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py index a54256c59a..324496c964 100644 --- a/openpype/hosts/maya/api/lib_renderproducts.py +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -339,7 +339,7 @@ class ARenderProducts: aov_tokens = ["", ""] def match_last(tokens, text): - """regex match the last occurence from a list of tokens""" + """regex match the last occurrence from a list of tokens""" pattern = "(?:.*)({})".format("|".join(tokens)) return re.search(pattern, text, re.IGNORECASE) @@ -1051,7 +1051,7 @@ class RenderProductsRedshift(ARenderProducts): def get_files(self, product): # When outputting AOVs we need to replace Redshift specific AOV tokens # with Maya render tokens for generating file sequences. We validate to - # a specific AOV fileprefix so we only need to accout for one + # a specific AOV fileprefix so we only need to account for one # replacement. if not product.multipart and product.driver: file_prefix = self._get_attr(product.driver + ".filePrefix") diff --git a/openpype/hosts/maya/api/workfile_template_builder.py b/openpype/hosts/maya/api/workfile_template_builder.py index 90ab6e21e0..4bee0664ef 100644 --- a/openpype/hosts/maya/api/workfile_template_builder.py +++ b/openpype/hosts/maya/api/workfile_template_builder.py @@ -33,7 +33,7 @@ class MayaTemplateBuilder(AbstractTemplateBuilder): get_template_preset implementation) Returns: - bool: Wether the template was succesfully imported or not + bool: Whether the template was successfully imported or not """ if cmds.objExists(PLACEHOLDER_SET): @@ -116,7 +116,7 @@ class MayaPlaceholderLoadPlugin(PlaceholderPlugin, PlaceholderLoadMixin): placeholder_name_parts = placeholder_data["builder_type"].split("_") pos = 1 - # add famlily in any + # add family in any placeholder_family = placeholder_data["family"] if placeholder_family: placeholder_name_parts.insert(pos, placeholder_family) diff --git a/openpype/hosts/maya/plugins/load/actions.py b/openpype/hosts/maya/plugins/load/actions.py index 2574624dbb..ba69debc40 100644 --- a/openpype/hosts/maya/plugins/load/actions.py +++ b/openpype/hosts/maya/plugins/load/actions.py @@ -118,7 +118,7 @@ class ImportMayaLoader(load.LoaderPlugin): "clean_import", label="Clean import", default=False, - help="Should all occurences of cbId be purged?" + help="Should all occurrences of cbId be purged?" ) ] diff --git a/openpype/hosts/maya/plugins/load/load_arnold_standin.py b/openpype/hosts/maya/plugins/load/load_arnold_standin.py index 11a2bd1966..21b2246f6c 100644 --- a/openpype/hosts/maya/plugins/load/load_arnold_standin.py +++ b/openpype/hosts/maya/plugins/load/load_arnold_standin.py @@ -180,7 +180,7 @@ class ArnoldStandinLoader(load.LoaderPlugin): proxy_basename, proxy_path = self._get_proxy_path(path) # Whether there is proxy or so, we still update the string operator. - # If no proxy exists, the string operator wont replace anything. + # If no proxy exists, the string operator won't replace anything. cmds.setAttr( string_replace_operator + ".match", "resources/" + proxy_basename, diff --git a/openpype/hosts/maya/plugins/publish/collect_multiverse_look.py b/openpype/hosts/maya/plugins/publish/collect_multiverse_look.py index a7cb14855b..33fc7a025f 100644 --- a/openpype/hosts/maya/plugins/publish/collect_multiverse_look.py +++ b/openpype/hosts/maya/plugins/publish/collect_multiverse_look.py @@ -255,7 +255,7 @@ class CollectMultiverseLookData(pyblish.api.InstancePlugin): Searches through the overrides finding all material overrides. From there it extracts the shading group and then finds all texture files in the shading group network. It also checks for mipmap versions of texture files - and adds them to the resouces to get published. + and adds them to the resources to get published. """ diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 447c9a615c..0572073d7d 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -95,7 +95,7 @@ def maketx(source, destination, args, logger): try: out = run_subprocess(subprocess_args) except Exception: - logger.error("Maketx converion failed", exc_info=True) + logger.error("Maketx conversion failed", exc_info=True) raise return out diff --git a/openpype/hosts/maya/plugins/publish/extract_multiverse_usd_over.py b/openpype/hosts/maya/plugins/publish/extract_multiverse_usd_over.py index 0628623e88..cf610ac6b4 100644 --- a/openpype/hosts/maya/plugins/publish/extract_multiverse_usd_over.py +++ b/openpype/hosts/maya/plugins/publish/extract_multiverse_usd_over.py @@ -102,7 +102,7 @@ class ExtractMultiverseUsdOverride(publish.Extractor): long=True) self.log.info("Collected object {}".format(members)) - # TODO: Deal with asset, composition, overide with options. + # TODO: Deal with asset, composition, override with options. import multiverse time_opts = None diff --git a/openpype/hosts/maya/plugins/publish/reset_xgen_attributes.py b/openpype/hosts/maya/plugins/publish/reset_xgen_attributes.py index b90885663c..d8e8554b68 100644 --- a/openpype/hosts/maya/plugins/publish/reset_xgen_attributes.py +++ b/openpype/hosts/maya/plugins/publish/reset_xgen_attributes.py @@ -30,7 +30,7 @@ class ResetXgenAttributes(pyblish.api.InstancePlugin): cmds.setAttr(palette + ".xgExportAsDelta", True) # Need to save the scene, cause the attribute changes above does not - # mark the scene as modified so user can exit without commiting the + # mark the scene as modified so user can exit without committing the # changes. self.log.info("Saving changes.") cmds.file(save=True) diff --git a/openpype/hosts/maya/plugins/publish/validate_camera_attributes.py b/openpype/hosts/maya/plugins/publish/validate_camera_attributes.py index bd1529e252..13ea53a357 100644 --- a/openpype/hosts/maya/plugins/publish/validate_camera_attributes.py +++ b/openpype/hosts/maya/plugins/publish/validate_camera_attributes.py @@ -8,7 +8,7 @@ from openpype.pipeline.publish import ValidateContentsOrder class ValidateCameraAttributes(pyblish.api.InstancePlugin): """Validates Camera has no invalid attribute keys or values. - The Alembic file format does not a specifc subset of attributes as such + The Alembic file format does not a specific subset of attributes as such we validate that no values are set there as the output will not match the current scene. For example the preScale, film offsets and film roll. diff --git a/openpype/hosts/maya/plugins/publish/validate_maya_units.py b/openpype/hosts/maya/plugins/publish/validate_maya_units.py index 357dde692c..011df0846c 100644 --- a/openpype/hosts/maya/plugins/publish/validate_maya_units.py +++ b/openpype/hosts/maya/plugins/publish/validate_maya_units.py @@ -34,7 +34,7 @@ class ValidateMayaUnits(pyblish.api.ContextPlugin): fps = context.data.get('fps') - # TODO repace query with using 'context.data["assetEntity"]' + # TODO replace query with using 'context.data["assetEntity"]' asset_doc = get_current_project_asset() asset_fps = mayalib.convert_to_maya_fps(asset_doc["data"]["fps"]) @@ -86,7 +86,7 @@ class ValidateMayaUnits(pyblish.api.ContextPlugin): cls.log.debug(current_linear) cls.log.info("Setting time unit to match project") - # TODO repace query with using 'context.data["assetEntity"]' + # TODO replace query with using 'context.data["assetEntity"]' asset_doc = get_current_project_asset() asset_fps = asset_doc["data"]["fps"] mayalib.set_scene_fps(asset_fps) diff --git a/openpype/hosts/maya/plugins/publish/validate_mvlook_contents.py b/openpype/hosts/maya/plugins/publish/validate_mvlook_contents.py index e583c1edba..36971bb144 100644 --- a/openpype/hosts/maya/plugins/publish/validate_mvlook_contents.py +++ b/openpype/hosts/maya/plugins/publish/validate_mvlook_contents.py @@ -42,7 +42,7 @@ class ValidateMvLookContents(pyblish.api.InstancePlugin): resources = instance.data.get("resources", []) for resource in resources: files = resource["files"] - self.log.debug("Resouce '{}', files: [{}]".format(resource, files)) + self.log.debug("Resource '{}', files: [{}]".format(resource, files)) node = resource["node"] if len(files) == 0: self.log.error("File node '{}' uses no or non-existing " diff --git a/openpype/hosts/maya/plugins/publish/validate_renderlayer_aovs.py b/openpype/hosts/maya/plugins/publish/validate_renderlayer_aovs.py index 6b6fb03eec..7919a6eaa1 100644 --- a/openpype/hosts/maya/plugins/publish/validate_renderlayer_aovs.py +++ b/openpype/hosts/maya/plugins/publish/validate_renderlayer_aovs.py @@ -37,8 +37,8 @@ class ValidateRenderLayerAOVs(pyblish.api.InstancePlugin): project_name = legacy_io.active_project() asset_doc = instance.data["assetEntity"] - render_passses = instance.data.get("renderPasses", []) - for render_pass in render_passses: + render_passes = instance.data.get("renderPasses", []) + for render_pass in render_passes: is_valid = self.validate_subset_registered( project_name, asset_doc, render_pass ) diff --git a/openpype/hosts/maya/plugins/publish/validate_transform_naming_suffix.py b/openpype/hosts/maya/plugins/publish/validate_transform_naming_suffix.py index 0147aa8a52..b2a83a80fb 100644 --- a/openpype/hosts/maya/plugins/publish/validate_transform_naming_suffix.py +++ b/openpype/hosts/maya/plugins/publish/validate_transform_naming_suffix.py @@ -21,7 +21,7 @@ class ValidateTransformNamingSuffix(pyblish.api.InstancePlugin): - nurbsSurface: _NRB - locator: _LOC - null/group: _GRP - Suffices can also be overriden by project settings. + Suffices can also be overridden by project settings. .. warning:: This grabs the first child shape as a reference and doesn't use the diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 2a14096f0e..157a02b9aa 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -148,7 +148,7 @@ def get_main_window(): def set_node_data(node, knobname, data): """Write data to node invisible knob - Will create new in case it doesnt exists + Will create new in case it doesn't exists or update the one already created. Args: @@ -506,7 +506,7 @@ def get_avalon_knob_data(node, prefix="avalon:", create=True): try: # check if data available on the node test = node[AVALON_DATA_GROUP].value() - log.debug("Only testing if data avalable: `{}`".format(test)) + log.debug("Only testing if data available: `{}`".format(test)) except NameError as e: # if it doesn't then create it log.debug("Creating avalon knob: `{}`".format(e)) @@ -908,11 +908,11 @@ def get_view_process_node(): continue if not ipn_node: - # in case a Viewer node is transfered from + # in case a Viewer node is transferred from # different workfile with old values raise NameError(( "Input process node name '{}' set in " - "Viewer '{}' is does't exists in nodes" + "Viewer '{}' is doesn't exists in nodes" ).format(ipn, v_.name())) ipn_node.setSelected(True) @@ -1662,7 +1662,7 @@ def create_write_node_legacy( tile_color = _data.get("tile_color", "0xff0000ff") GN["tile_color"].setValue(tile_color) - # overrie knob values from settings + # override knob values from settings for knob in knob_overrides: knob_type = knob["type"] knob_name = knob["name"] @@ -2117,7 +2117,7 @@ class WorkfileSettings(object): write_node[knob["name"]].setValue(value) except TypeError: log.warning( - "Legacy workflow didnt work, switching to current") + "Legacy workflow didn't work, switching to current") set_node_knobs_from_settings( write_node, nuke_imageio_writes["knobs"]) @@ -2543,7 +2543,7 @@ def reset_selection(): def select_nodes(nodes): - """Selects all inputed nodes + """Selects all inputted nodes Arguments: nodes (list): nuke nodes to be selected @@ -2560,7 +2560,7 @@ def launch_workfiles_app(): Trigger to show workfiles tool on application launch. Can be executed only once all other calls are ignored. - Workfiles tool show is deffered after application initialization using + Workfiles tool show is deferred after application initialization using QTimer. """ @@ -2581,7 +2581,7 @@ def launch_workfiles_app(): # Show workfiles tool using timer # - this will be probably triggered during initialization in that case # the application is not be able to show uis so it must be - # deffered using timer + # deferred using timer # - timer should be processed when initialization ends # When applications starts to process events. timer = QtCore.QTimer() diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index aec87be5ab..cc3af2a38f 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -594,7 +594,7 @@ class ExporterReview(object): Defaults to None. range (bool, optional): flag for adding ranges. Defaults to False. - custom_tags (list[str], optional): user inputed custom tags. + custom_tags (list[str], optional): user inputted custom tags. Defaults to None. """ add_tags = tags or [] @@ -1110,7 +1110,7 @@ class AbstractWriteRender(OpenPypeCreator): def is_legacy(self): """Check if it needs to run legacy code - In case where `type` key is missing in singe + In case where `type` key is missing in single knob it is legacy project anatomy. Returns: diff --git a/openpype/hosts/nuke/api/utils.py b/openpype/hosts/nuke/api/utils.py index 6bcb752dd1..2b3c35c23a 100644 --- a/openpype/hosts/nuke/api/utils.py +++ b/openpype/hosts/nuke/api/utils.py @@ -87,7 +87,7 @@ def bake_gizmos_recursively(in_group=None): def colorspace_exists_on_node(node, colorspace_name): """ Check if colorspace exists on node - Look through all options in the colorpsace knob, and see if we have an + Look through all options in the colorspace knob, and see if we have an exact match to one of the items. Args: diff --git a/openpype/hosts/nuke/api/workfile_template_builder.py b/openpype/hosts/nuke/api/workfile_template_builder.py index fb0afb3d55..cf85a5ea05 100644 --- a/openpype/hosts/nuke/api/workfile_template_builder.py +++ b/openpype/hosts/nuke/api/workfile_template_builder.py @@ -42,7 +42,7 @@ class NukeTemplateBuilder(AbstractTemplateBuilder): get_template_preset implementation) Returns: - bool: Wether the template was successfully imported or not + bool: Whether the template was successfully imported or not """ # TODO check if the template is already imported @@ -222,7 +222,7 @@ class NukePlaceholderLoadPlugin(NukePlaceholderPlugin, PlaceholderLoadMixin): self._imprint_siblings(placeholder) if placeholder.data["nb_children"] == 0: - # save initial nodes postions and dimensions, update them + # save initial nodes positions and dimensions, update them # and set inputs and outputs of loaded nodes self._imprint_inits() @@ -231,7 +231,7 @@ class NukePlaceholderLoadPlugin(NukePlaceholderPlugin, PlaceholderLoadMixin): elif placeholder.data["siblings"]: # create copies of placeholder siblings for the new loaded nodes, - # set their inputs and outpus and update all nodes positions and + # set their inputs and outputs and update all nodes positions and # dimensions and siblings names siblings = get_nodes_by_names(placeholder.data["siblings"]) @@ -632,7 +632,7 @@ class NukePlaceholderCreatePlugin( self._imprint_siblings(placeholder) if placeholder.data["nb_children"] == 0: - # save initial nodes postions and dimensions, update them + # save initial nodes positions and dimensions, update them # and set inputs and outputs of created nodes self._imprint_inits() @@ -641,7 +641,7 @@ class NukePlaceholderCreatePlugin( elif placeholder.data["siblings"]: # create copies of placeholder siblings for the new created nodes, - # set their inputs and outpus and update all nodes positions and + # set their inputs and outputs and update all nodes positions and # dimensions and siblings names siblings = get_nodes_by_names(placeholder.data["siblings"]) diff --git a/openpype/hosts/nuke/plugins/create/convert_legacy.py b/openpype/hosts/nuke/plugins/create/convert_legacy.py index d7341c625f..c143e4cb27 100644 --- a/openpype/hosts/nuke/plugins/create/convert_legacy.py +++ b/openpype/hosts/nuke/plugins/create/convert_legacy.py @@ -39,7 +39,7 @@ class LegacyConverted(SubsetConvertorPlugin): break if legacy_found: - # if not item do not add legacy instance convertor + # if not item do not add legacy instance converter self.add_convertor_item("Convert legacy instances") def convert(self): diff --git a/openpype/hosts/nuke/plugins/create/create_source.py b/openpype/hosts/nuke/plugins/create/create_source.py index 06cf4e6cbf..57504b5d53 100644 --- a/openpype/hosts/nuke/plugins/create/create_source.py +++ b/openpype/hosts/nuke/plugins/create/create_source.py @@ -85,4 +85,4 @@ class CreateSource(NukeCreator): raise NukeCreatorError("Creator error: No active selection") else: NukeCreatorError( - "Creator error: only supprted with active selection") + "Creator error: only supported with active selection") diff --git a/openpype/hosts/nuke/plugins/publish/collect_writes.py b/openpype/hosts/nuke/plugins/publish/collect_writes.py index 0008a756bc..536a0698f3 100644 --- a/openpype/hosts/nuke/plugins/publish/collect_writes.py +++ b/openpype/hosts/nuke/plugins/publish/collect_writes.py @@ -189,7 +189,7 @@ class CollectNukeWrites(pyblish.api.InstancePlugin, }) # make sure rendered sequence on farm will - # be used for exctract review + # be used for extract review if not instance.data["review"]: instance.data["useSequenceForReview"] = False diff --git a/openpype/hosts/nuke/plugins/publish/validate_backdrop.py b/openpype/hosts/nuke/plugins/publish/validate_backdrop.py index 208d4a2498..5f4a5c3ab0 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_backdrop.py +++ b/openpype/hosts/nuke/plugins/publish/validate_backdrop.py @@ -48,7 +48,7 @@ class SelectCenterInNodeGraph(pyblish.api.Action): class ValidateBackdrop(pyblish.api.InstancePlugin): """ Validate amount of nodes on backdrop node in case user - forgoten to add nodes above the publishing backdrop node. + forgotten to add nodes above the publishing backdrop node. """ order = pyblish.api.ValidatorOrder diff --git a/openpype/hosts/photoshop/api/extension/host/index.jsx b/openpype/hosts/photoshop/api/extension/host/index.jsx index 2acec1ebc1..e2711fb960 100644 --- a/openpype/hosts/photoshop/api/extension/host/index.jsx +++ b/openpype/hosts/photoshop/api/extension/host/index.jsx @@ -199,7 +199,7 @@ function getActiveDocumentName(){ function getActiveDocumentFullName(){ /** * Returns file name of active document with file path. - * activeDocument.fullName returns path in URI (eg /c/.. insted of c:/) + * activeDocument.fullName returns path in URI (eg /c/.. instead of c:/) * */ if (documents.length == 0){ return null; @@ -225,7 +225,7 @@ function getSelectedLayers(doc) { * Returns json representation of currently selected layers. * Works in three steps - 1) creates new group with selected layers * 2) traverses this group - * 3) deletes newly created group, not neede + * 3) deletes newly created group, not needed * Bit weird, but Adobe.. **/ if (doc == null){ @@ -284,7 +284,7 @@ function selectLayers(selectedLayers){ existing_ids.push(existing_layers[y]["id"]); } for (var i = 0; i < selectedLayers.length; i++) { - // a check to see if the id stil exists + // a check to see if the id still exists var id = selectedLayers[i]; if(existing_ids.toString().indexOf(id)>=0){ layers[i] = charIDToTypeID( "Lyr " ); diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index f41eb36caf..b3ad20df39 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -250,7 +250,7 @@ def create_timeline_item(media_pool_item: object, media_pool_item, timeline) assert output_timeline_item, AssertionError( - "Track Item with name `{}` doesnt exist on the timeline: `{}`".format( + "Track Item with name `{}` doesn't exist on the timeline: `{}`".format( clip_name, timeline.GetName() )) return output_timeline_item @@ -571,7 +571,7 @@ def create_compound_clip(clip_data, name, folder): # Set current folder to input media_pool_folder: mp.SetCurrentFolder(folder) - # check if clip doesnt exist already: + # check if clip doesn't exist already: clips = folder.GetClipList() cct = next((c for c in clips if c.GetName() in name), None) @@ -582,7 +582,7 @@ def create_compound_clip(clip_data, name, folder): # Create empty timeline in current folder and give name: cct = mp.CreateEmptyTimeline(name) - # check if clip doesnt exist already: + # check if clip doesn't exist already: clips = folder.GetClipList() cct = next((c for c in clips if c.GetName() in name), None) diff --git a/openpype/hosts/resolve/api/menu_style.qss b/openpype/hosts/resolve/api/menu_style.qss index d2d3d1ed37..3d51c7139f 100644 --- a/openpype/hosts/resolve/api/menu_style.qss +++ b/openpype/hosts/resolve/api/menu_style.qss @@ -61,7 +61,7 @@ QVBoxLayout { background-color: #282828; } -#Devider { +#Divider { border: 1px solid #090909; background-color: #585858; } diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index 77e30149fd..4fa73e82cd 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -715,7 +715,7 @@ class PublishClip: # increasing steps by index of rename iteration self.count_steps *= self.rename_index - hierarchy_formating_data = dict() + hierarchy_formatting_data = dict() _data = self.timeline_item_default_data.copy() if self.ui_inputs: # adding tag metadata from ui @@ -749,13 +749,13 @@ class PublishClip: # fill up pythonic expresisons in hierarchy data for k, _v in self.hierarchy_data.items(): - hierarchy_formating_data[k] = _v["value"].format(**_data) + hierarchy_formatting_data[k] = _v["value"].format(**_data) else: # if no gui mode then just pass default data - hierarchy_formating_data = self.hierarchy_data + hierarchy_formatting_data = self.hierarchy_data tag_hierarchy_data = self._solve_tag_hierarchy_data( - hierarchy_formating_data + hierarchy_formatting_data ) tag_hierarchy_data.update({"heroTrack": True}) @@ -793,17 +793,17 @@ class PublishClip: self.tag_data.update({"reviewTrack": None}) - def _solve_tag_hierarchy_data(self, hierarchy_formating_data): + def _solve_tag_hierarchy_data(self, hierarchy_formatting_data): """ Solve tag data from hierarchy data and templates. """ # fill up clip name and hierarchy keys - hierarchy_filled = self.hierarchy.format(**hierarchy_formating_data) - clip_name_filled = self.clip_name.format(**hierarchy_formating_data) + hierarchy_filled = self.hierarchy.format(**hierarchy_formatting_data) + clip_name_filled = self.clip_name.format(**hierarchy_formatting_data) return { "newClipName": clip_name_filled, "hierarchy": hierarchy_filled, "parents": self.parents, - "hierarchyData": hierarchy_formating_data, + "hierarchyData": hierarchy_formatting_data, "subset": self.subset, "family": self.subset_family, "families": ["clip"] diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_bulk_mov_instances.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_bulk_mov_instances.py index 7925b0ecf3..6c3b0c3efd 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_bulk_mov_instances.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_bulk_mov_instances.py @@ -83,9 +83,9 @@ class CollectBulkMovInstances(pyblish.api.InstancePlugin): self.log.info(f"Created new instance: {instance_name}") - def convertor(value): + def converter(value): return str(value) self.log.debug("Instance data: {}".format( - json.dumps(new_instance.data, indent=4, default=convertor) + json.dumps(new_instance.data, indent=4, default=converter) )) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_context.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_context.py index 2bf3917e2f..a7746530e7 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_context.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_context.py @@ -104,7 +104,7 @@ class CollectContextDataSAPublish(pyblish.api.ContextPlugin): if repr.get(k): repr.pop(k) - # convert files to list if it isnt + # convert files to list if it isn't if not isinstance(files, (tuple, list)): files = [files] @@ -174,7 +174,7 @@ class CollectContextDataSAPublish(pyblish.api.ContextPlugin): continue files = repre["files"] - # Convert files to list if it isnt + # Convert files to list if it isn't if not isinstance(files, (tuple, list)): files = [files] diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial.py index 8633d4bf9d..391cace761 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial.py @@ -116,7 +116,7 @@ class CollectEditorial(pyblish.api.InstancePlugin): kwargs = {} if extension == ".edl": # EDL has no frame rate embedded so needs explicit - # frame rate else 24 is asssumed. + # frame rate else 24 is assumed. kwargs["rate"] = get_current_project_asset()["data"]["fps"] instance.data["otio_timeline"] = otio.adapters.read_from_file( diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_frame_ranges.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_frame_ranges.py index 074c62ea0e..e46fbe6098 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/validate_frame_ranges.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_frame_ranges.py @@ -29,7 +29,7 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): for pattern in self.skip_timelines_check): self.log.info("Skipping for {} task".format(instance.data["task"])) - # TODO repace query with using 'instance.data["assetEntity"]' + # TODO replace query with using 'instance.data["assetEntity"]' asset_data = get_current_project_asset(instance.data["asset"])["data"] frame_start = asset_data["frameStart"] frame_end = asset_data["frameEnd"] diff --git a/openpype/hosts/traypublisher/api/editorial.py b/openpype/hosts/traypublisher/api/editorial.py index 293db542a9..e8f76bd314 100644 --- a/openpype/hosts/traypublisher/api/editorial.py +++ b/openpype/hosts/traypublisher/api/editorial.py @@ -8,10 +8,10 @@ from openpype.pipeline.create import CreatorError class ShotMetadataSolver: """ Solving hierarchical metadata - Used during editorial publishing. Works with imput + Used during editorial publishing. Works with input clip name and settings defining python formatable template. Settings also define searching patterns - and its token keys used for formating in templates. + and its token keys used for formatting in templates. """ NO_DECOR_PATERN = re.compile(r"\{([a-z]*?)\}") @@ -40,13 +40,13 @@ class ShotMetadataSolver: """Shot renaming function Args: - data (dict): formating data + data (dict): formatting data Raises: CreatorError: If missing keys Returns: - str: formated new name + str: formatted new name """ shot_rename_template = self.shot_rename[ "shot_rename_template"] @@ -58,7 +58,7 @@ class ShotMetadataSolver: "Make sure all keys in settings are correct:: \n\n" f"From template string {shot_rename_template} > " f"`{_E}` has no equivalent in \n" - f"{list(data.keys())} input formating keys!" + f"{list(data.keys())} input formatting keys!" )) def _generate_tokens(self, clip_name, source_data): @@ -68,7 +68,7 @@ class ShotMetadataSolver: Args: clip_name (str): name of clip in editorial - source_data (dict): data for formating + source_data (dict): data for formatting Raises: CreatorError: if missing key @@ -106,14 +106,14 @@ class ShotMetadataSolver: return output_data def _create_parents_from_settings(self, parents, data): - """Formating parent components. + """formatting parent components. Args: parents (list): list of dict parent components - data (dict): formating data + data (dict): formatting data Raises: - CreatorError: missing formating key + CreatorError: missing formatting key CreatorError: missing token key KeyError: missing parent token @@ -126,7 +126,7 @@ class ShotMetadataSolver: # fill parent keys data template from anatomy data try: - _parent_tokens_formating_data = { + _parent_tokens_formatting_data = { parent_token["name"]: parent_token["value"].format(**data) for parent_token in hierarchy_parents } @@ -143,17 +143,17 @@ class ShotMetadataSolver: for _index, _parent in enumerate( shot_hierarchy["parents_path"].split("/") ): - # format parent token with value which is formated + # format parent token with value which is formatted try: parent_name = _parent.format( - **_parent_tokens_formating_data) + **_parent_tokens_formatting_data) except KeyError as _E: raise CreatorError(( "Make sure all keys in settings are correct : \n\n" f"`{_E}` from template string " f"{shot_hierarchy['parents_path']}, " f" has no equivalent in \n" - f"{list(_parent_tokens_formating_data.keys())} parents" + f"{list(_parent_tokens_formatting_data.keys())} parents" )) parent_token_name = ( @@ -225,7 +225,7 @@ class ShotMetadataSolver: visual_hierarchy = [asset_doc] current_doc = asset_doc - # looping trought all available visual parents + # looping through all available visual parents # if they are not available anymore than it breaks while True: visual_parent_id = current_doc["data"]["visualParent"] @@ -288,7 +288,7 @@ class ShotMetadataSolver: Args: clip_name (str): clip name - source_data (dict): formating data + source_data (dict): formatting data Returns: (str, dict): shot name and hierarchy data @@ -301,19 +301,19 @@ class ShotMetadataSolver: # match clip to shot name at start shot_name = clip_name - # parse all tokens and generate formating data - formating_data = self._generate_tokens(shot_name, source_data) + # parse all tokens and generate formatting data + formatting_data = self._generate_tokens(shot_name, source_data) # generate parents from selected asset parents = self._get_parents_from_selected_asset(asset_doc, project_doc) if self.shot_rename["enabled"]: - shot_name = self._rename_template(formating_data) + shot_name = self._rename_template(formatting_data) self.log.info(f"Renamed shot name: {shot_name}") if self.shot_hierarchy["enabled"]: parents = self._create_parents_from_settings( - parents, formating_data) + parents, formatting_data) if self.shot_add_tasks: tasks = self._generate_tasks_from_settings( diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index 73be43444e..0630dfb3da 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -260,7 +260,7 @@ or updating already created. Publishing will create OTIO file. ) if not first_otio_timeline: - # assing otio timeline for multi file to layer + # assign otio timeline for multi file to layer first_otio_timeline = otio_timeline # create otio editorial instance @@ -283,7 +283,7 @@ or updating already created. Publishing will create OTIO file. Args: subset_name (str): name of subset - data (dict): instnance data + data (dict): instance data sequence_path (str): path to sequence file media_path (str): path to media file otio_timeline (otio.Timeline): otio timeline object @@ -315,7 +315,7 @@ or updating already created. Publishing will create OTIO file. kwargs = {} if extension == ".edl": # EDL has no frame rate embedded so needs explicit - # frame rate else 24 is asssumed. + # frame rate else 24 is assumed. kwargs["rate"] = fps kwargs["ignore_timecode_mismatch"] = True @@ -358,7 +358,7 @@ or updating already created. Publishing will create OTIO file. sequence_file_name, first_otio_timeline=None ): - """Helping function fro creating clip instance + """Helping function for creating clip instance Args: otio_timeline (otio.Timeline): otio timeline object @@ -527,7 +527,7 @@ or updating already created. Publishing will create OTIO file. Args: otio_clip (otio.Clip): otio clip object - preset (dict): sigle family preset + preset (dict): single family preset instance_data (dict): instance data parenting_data (dict): shot instance parent data @@ -767,7 +767,7 @@ or updating already created. Publishing will create OTIO file. ] def _validate_clip_for_processing(self, otio_clip): - """Validate otio clip attribues + """Validate otio clip attributes Args: otio_clip (otio.Clip): otio clip object @@ -843,7 +843,7 @@ or updating already created. Publishing will create OTIO file. single_item=False, label="Media files", ), - # TODO: perhpas better would be timecode and fps input + # TODO: perhaps better would be timecode and fps input NumberDef( "timeline_offset", default=0, diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_simple_instances.py b/openpype/hosts/traypublisher/plugins/publish/collect_simple_instances.py index 183195a515..c081216481 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_simple_instances.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_simple_instances.py @@ -14,7 +14,7 @@ class CollectSettingsSimpleInstances(pyblish.api.InstancePlugin): There is also possibility to have reviewable representation which can be stored under 'reviewable' attribute stored on instance data. If there was - already created representation with the same files as 'revieable' containes + already created representation with the same files as 'reviewable' contains Representations can be marked for review and in that case is also added 'review' family to instance families. For review can be marked only one diff --git a/openpype/hosts/tvpaint/plugins/publish/help/validate_layers_visibility.xml b/openpype/hosts/tvpaint/plugins/publish/help/validate_layers_visibility.xml index e7be735888..5832c74350 100644 --- a/openpype/hosts/tvpaint/plugins/publish/help/validate_layers_visibility.xml +++ b/openpype/hosts/tvpaint/plugins/publish/help/validate_layers_visibility.xml @@ -1,7 +1,7 @@ -Layers visiblity +Layers visibility ## All layers are not visible Layers visibility was changed during publishing which caused that all layers for subset "{instance_name}" are hidden. diff --git a/openpype/hosts/tvpaint/plugins/publish/help/validate_workfile_metadata.xml b/openpype/hosts/tvpaint/plugins/publish/help/validate_workfile_metadata.xml index 7397f6ef0b..0fc03c2948 100644 --- a/openpype/hosts/tvpaint/plugins/publish/help/validate_workfile_metadata.xml +++ b/openpype/hosts/tvpaint/plugins/publish/help/validate_workfile_metadata.xml @@ -11,7 +11,7 @@ Your scene does not contain metadata about {missing_metadata}. Resave the scene using Workfiles tool or hit the "Repair" button on the right. -### How this could happend? +### How this could happen? You're using scene file that was not created using Workfiles tool. diff --git a/openpype/hosts/tvpaint/plugins/publish/help/validate_workfile_project_name.xml b/openpype/hosts/tvpaint/plugins/publish/help/validate_workfile_project_name.xml index c4ffafc8b5..bb57e93bf2 100644 --- a/openpype/hosts/tvpaint/plugins/publish/help/validate_workfile_project_name.xml +++ b/openpype/hosts/tvpaint/plugins/publish/help/validate_workfile_project_name.xml @@ -13,7 +13,7 @@ If the workfile belongs to project "{env_project_name}" then use Workfiles tool Otherwise close TVPaint and launch it again from project you want to publish in. -### How this could happend? +### How this could happen? You've opened workfile from different project. You've opened TVPaint on a task from "{env_project_name}" then you've opened TVPaint again on task from "{workfile_project_name}" without closing the TVPaint. Because TVPaint can run only once the project didn't change. diff --git a/openpype/hosts/tvpaint/plugins/publish/validate_asset_name.py b/openpype/hosts/tvpaint/plugins/publish/validate_asset_name.py index d7984ce971..9347960d3f 100644 --- a/openpype/hosts/tvpaint/plugins/publish/validate_asset_name.py +++ b/openpype/hosts/tvpaint/plugins/publish/validate_asset_name.py @@ -38,7 +38,7 @@ class ValidateAssetName( OptionalPyblishPluginMixin, pyblish.api.ContextPlugin ): - """Validate assset name present on instance. + """Validate asset name present on instance. Asset name on instance should be the same as context's. """ diff --git a/openpype/hosts/unreal/api/pipeline.py b/openpype/hosts/unreal/api/pipeline.py index 8a5a459194..1a7c626984 100644 --- a/openpype/hosts/unreal/api/pipeline.py +++ b/openpype/hosts/unreal/api/pipeline.py @@ -306,7 +306,7 @@ def imprint(node, data): def show_tools_popup(): """Show popup with tools. - Popup will disappear on click or loosing focus. + Popup will disappear on click or losing focus. """ from openpype.hosts.unreal.api import tools_ui diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeLib.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeLib.cpp index 008025e816..34faba1f49 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeLib.cpp +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeLib.cpp @@ -31,7 +31,7 @@ bool UOpenPypeLib::SetFolderColor(const FString& FolderPath, const FLinearColor& } /** - * Returns all poperties on given object + * Returns all properties on given object * @param cls - class * @return TArray of properties */ diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/Commandlets/OPActionResult.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/Commandlets/OPActionResult.h index c960bbf190..322a23a3e8 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/Commandlets/OPActionResult.h +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/Commandlets/OPActionResult.h @@ -16,7 +16,7 @@ /** * @brief This enum values are humanly readable mapping of error codes. * Here should be all error codes to be possible find what went wrong. -* TODO: In the future should exists an web document where is mapped error code & what problem occured & how to repair it... +* TODO: In the future a web document should exists with the mapped error code & what problem occurred & how to repair it... */ UENUM() namespace EOP_ActionResult @@ -27,11 +27,11 @@ namespace EOP_ActionResult ProjectNotCreated, ProjectNotLoaded, ProjectNotSaved, - //....Here insert another values + //....Here insert another values //Do not remove! //Usable for looping through enum values - __Last UMETA(Hidden) + __Last UMETA(Hidden) }; } @@ -63,10 +63,10 @@ public: private: /** @brief Action status */ - EOP_ActionResult::Type Status; + EOP_ActionResult::Type Status; /** @brief Optional reason of fail */ - FText Reason; + FText Reason; public: /** @@ -77,7 +77,7 @@ public: EOP_ActionResult::Type& GetStatus(); FText& GetReason(); -private: +private: void TryLog() const; }; diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeLib.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeLib.cpp index 008025e816..34faba1f49 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeLib.cpp +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeLib.cpp @@ -31,7 +31,7 @@ bool UOpenPypeLib::SetFolderColor(const FString& FolderPath, const FLinearColor& } /** - * Returns all poperties on given object + * Returns all properties on given object * @param cls - class * @return TArray of properties */ diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Commandlets/OPActionResult.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Commandlets/OPActionResult.h index c960bbf190..322a23a3e8 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Commandlets/OPActionResult.h +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Commandlets/OPActionResult.h @@ -16,7 +16,7 @@ /** * @brief This enum values are humanly readable mapping of error codes. * Here should be all error codes to be possible find what went wrong. -* TODO: In the future should exists an web document where is mapped error code & what problem occured & how to repair it... +* TODO: In the future a web document should exists with the mapped error code & what problem occurred & how to repair it... */ UENUM() namespace EOP_ActionResult @@ -27,11 +27,11 @@ namespace EOP_ActionResult ProjectNotCreated, ProjectNotLoaded, ProjectNotSaved, - //....Here insert another values + //....Here insert another values //Do not remove! //Usable for looping through enum values - __Last UMETA(Hidden) + __Last UMETA(Hidden) }; } @@ -63,10 +63,10 @@ public: private: /** @brief Action status */ - EOP_ActionResult::Type Status; + EOP_ActionResult::Type Status; /** @brief Optional reason of fail */ - FText Reason; + FText Reason; public: /** @@ -77,7 +77,7 @@ public: EOP_ActionResult::Type& GetStatus(); FText& GetReason(); -private: +private: void TryLog() const; }; diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index ca6b0ce736..2496440e5f 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -171,7 +171,7 @@ class CameraLoader(plugin.Loader): project_name = legacy_io.active_project() # TODO refactor - # - Creationg of hierarchy should be a function in unreal integration + # - 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 diff --git a/openpype/hosts/webpublisher/lib.py b/openpype/hosts/webpublisher/lib.py index 4bc3f1db80..b207f85b46 100644 --- a/openpype/hosts/webpublisher/lib.py +++ b/openpype/hosts/webpublisher/lib.py @@ -30,7 +30,7 @@ def parse_json(path): Returns: (dict) or None if unparsable Raises: - AsssertionError if 'path' doesn't exist + AssertionError if 'path' doesn't exist """ path = path.strip('\"') assert os.path.isfile(path), ( diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index 127d31d042..8adae34827 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -969,7 +969,7 @@ class ApplicationLaunchContext: """Helper to collect application launch hooks from addons. Module have to have implemented 'get_launch_hook_paths' method which - can expect appliction as argument or nothing. + can expect application as argument or nothing. Returns: List[str]: Paths to launch hook directories. diff --git a/openpype/lib/attribute_definitions.py b/openpype/lib/attribute_definitions.py index b5cd15f41a..6054d2a92a 100644 --- a/openpype/lib/attribute_definitions.py +++ b/openpype/lib/attribute_definitions.py @@ -9,7 +9,7 @@ from abc import ABCMeta, abstractmethod, abstractproperty import six import clique -# Global variable which store attribude definitions by type +# Global variable which store attribute definitions by type # - default types are registered on import _attr_defs_by_type = {} @@ -93,7 +93,7 @@ class AbstractAttrDefMeta(ABCMeta): @six.add_metaclass(AbstractAttrDefMeta) class AbstractAttrDef(object): - """Abstraction of attribute definiton. + """Abstraction of attribute definition. Each attribute definition must have implemented validation and conversion method. @@ -427,7 +427,7 @@ class EnumDef(AbstractAttrDef): """Enumeration of single item from items. Args: - items: Items definition that can be coverted using + items: Items definition that can be converted using 'prepare_enum_items'. default: Default value. Must be one key(value) from passed items. """ diff --git a/openpype/lib/events.py b/openpype/lib/events.py index 096201312f..bed00fe659 100644 --- a/openpype/lib/events.py +++ b/openpype/lib/events.py @@ -156,7 +156,7 @@ class EventCallback(object): self._enabled = enabled def deregister(self): - """Calling this funcion will cause that callback will be removed.""" + """Calling this function will cause that callback will be removed.""" # Fake reference self._ref_valid = False diff --git a/openpype/lib/execute.py b/openpype/lib/execute.py index 6f9a095285..834394b02f 100644 --- a/openpype/lib/execute.py +++ b/openpype/lib/execute.py @@ -163,7 +163,7 @@ def run_subprocess(*args, **kwargs): def clean_envs_for_openpype_process(env=None): - """Modify environemnts that may affect OpenPype process. + """Modify environments that may affect OpenPype process. Main reason to implement this function is to pop PYTHONPATH which may be affected by in-host environments. diff --git a/openpype/lib/file_transaction.py b/openpype/lib/file_transaction.py index 81332a8891..80f4e81f2c 100644 --- a/openpype/lib/file_transaction.py +++ b/openpype/lib/file_transaction.py @@ -130,7 +130,7 @@ class FileTransaction(object): path_same = self._same_paths(src, dst) if path_same: self.log.debug( - "Source and destionation are same files {} -> {}".format( + "Source and destination are same files {} -> {}".format( src, dst)) continue diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index 799693554f..57968b3700 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -540,7 +540,7 @@ def convert_for_ffmpeg( continue # Remove attributes that have string value longer than allowed length - # for ffmpeg or when containt unallowed symbols + # for ffmpeg or when contain unallowed symbols erase_reason = "Missing reason" erase_attribute = False if len(attr_value) > MAX_FFMPEG_STRING_LEN: @@ -680,7 +680,7 @@ def convert_input_paths_for_ffmpeg( continue # Remove attributes that have string value longer than allowed - # length for ffmpeg or when containt unallowed symbols + # length for ffmpeg or when containing unallowed symbols erase_reason = "Missing reason" erase_attribute = False if len(attr_value) > MAX_FFMPEG_STRING_LEN: @@ -968,7 +968,7 @@ def _ffmpeg_dnxhd_codec_args(stream_data, source_ffmpeg_cmd): if source_ffmpeg_cmd: # Define bitrate arguments bit_rate_args = ("-b:v", "-vb",) - # Seprate the two variables in case something else should be copied + # Separate the two variables in case something else should be copied # from source command copy_args = [] copy_args.extend(bit_rate_args) diff --git a/openpype/lib/vendor_bin_utils.py b/openpype/lib/vendor_bin_utils.py index e5deb7a6b2..f27c78d486 100644 --- a/openpype/lib/vendor_bin_utils.py +++ b/openpype/lib/vendor_bin_utils.py @@ -260,7 +260,7 @@ def _oiio_executable_validation(filepath): that it can be executed. For that is used '--help' argument which is fast and does not need any other inputs. - Any possible crash of missing libraries or invalid build should be catched. + Any possible crash of missing libraries or invalid build should be caught. Main reason is to validate if executable can be executed on OS just running which can be issue ob linux machines. @@ -329,7 +329,7 @@ def _ffmpeg_executable_validation(filepath): that it can be executed. For that is used '-version' argument which is fast and does not need any other inputs. - Any possible crash of missing libraries or invalid build should be catched. + Any possible crash of missing libraries or invalid build should be caught. Main reason is to validate if executable can be executed on OS just running which can be issue ob linux machines. diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 0fd21492e8..730585212b 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -472,7 +472,7 @@ class OpenPypeModule: Args: application (Application): Application that is launched. - env (dict): Current environemnt variables. + env (dict): Current environment variables. """ pass @@ -622,7 +622,7 @@ class ModulesManager: # Check if class is abstract (Developing purpose) if inspect.isabstract(modules_item): - # Find missing implementations by convetion on `abc` module + # Find missing implementations by convention on `abc` module not_implemented = [] for attr_name in dir(modules_item): attr = getattr(modules_item, attr_name, None) @@ -708,13 +708,13 @@ class ModulesManager: ] def collect_global_environments(self): - """Helper to collect global enviornment variabled from modules. + """Helper to collect global environment variabled from modules. Returns: dict: Global environment variables from enabled modules. Raises: - AssertionError: Gobal environment variables must be unique for + AssertionError: Global environment variables must be unique for all modules. """ module_envs = {} @@ -1174,7 +1174,7 @@ class TrayModulesManager(ModulesManager): def get_module_settings_defs(): - """Check loaded addons/modules for existence of thei settings definition. + """Check loaded addons/modules for existence of their settings definition. Check if OpenPype addon/module as python module has class that inherit from `ModuleSettingsDef` in python module variables (imported @@ -1204,7 +1204,7 @@ def get_module_settings_defs(): continue if inspect.isabstract(attr): - # Find missing implementations by convetion on `abc` module + # Find missing implementations by convention on `abc` module not_implemented = [] for attr_name in dir(attr): attr = getattr(attr, attr_name, None) @@ -1293,7 +1293,7 @@ class BaseModuleSettingsDef: class ModuleSettingsDef(BaseModuleSettingsDef): - """Settings definiton with separated system and procect settings parts. + """Settings definition with separated system and procect settings parts. Reduce conditions that must be checked and adds predefined methods for each case. diff --git a/openpype/modules/clockify/clockify_api.py b/openpype/modules/clockify/clockify_api.py index 80979c83ab..47af002f7a 100644 --- a/openpype/modules/clockify/clockify_api.py +++ b/openpype/modules/clockify/clockify_api.py @@ -247,7 +247,7 @@ class ClockifyAPI: current_timer = self.get_in_progress() # Check if is currently run another times and has same values - # DO not restart the timer, if it is already running for curent task + # DO not restart the timer, if it is already running for current task if current_timer: current_timer_hierarchy = current_timer.get("description") current_project_id = current_timer.get("projectId") diff --git a/openpype/modules/clockify/clockify_module.py b/openpype/modules/clockify/clockify_module.py index 200a268ad7..b6efec7907 100644 --- a/openpype/modules/clockify/clockify_module.py +++ b/openpype/modules/clockify/clockify_module.py @@ -76,7 +76,7 @@ class ClockifyModule(OpenPypeModule, ITrayModule, IPluginPaths): return def get_plugin_paths(self): - """Implementaton of IPluginPaths to get plugin paths.""" + """Implementation of IPluginPaths to get plugin paths.""" actions_path = os.path.join( os.path.dirname(os.path.abspath(__file__)), "launcher_actions" ) diff --git a/openpype/modules/clockify/widgets.py b/openpype/modules/clockify/widgets.py index 8c28f38b6e..86e67569f2 100644 --- a/openpype/modules/clockify/widgets.py +++ b/openpype/modules/clockify/widgets.py @@ -34,7 +34,7 @@ class MessageWidget(QtWidgets.QWidget): def _ui_layout(self, messages): if not messages: - messages = ["*Misssing messages (This is a bug)*", ] + messages = ["*Missing messages (This is a bug)*", ] elif not isinstance(messages, (tuple, list)): messages = [messages, ] diff --git a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py index cc069cf51a..0c899a500c 100644 --- a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py @@ -66,7 +66,7 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin, ), NumberDef( "concurrency", - label="Concurency", + label="Concurrency", default=cls.concurrent_tasks, decimals=0, minimum=1, diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 0d0698c21f..f6a794fbc0 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -85,10 +85,10 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): These jobs are dependent on a deadline or muster job submission prior to this plug-in. - - In case of Deadline, it creates dependend job on farm publishing + - In case of Deadline, it creates dependent job on farm publishing rendered image sequence. - - In case of Muster, there is no need for such thing as dependend job, + - In case of Muster, there is no need for such thing as dependent job, post action will be executed and rendered sequence will be published. Options in instance.data: @@ -108,7 +108,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): - publishJobState (str, Optional): "Active" or "Suspended" This defaults to "Suspended" - - expectedFiles (list or dict): explained bellow + - expectedFiles (list or dict): explained below """ @@ -158,7 +158,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): # regex for finding frame number in string R_FRAME_NUMBER = re.compile(r'.+\.(?P[0-9]+)\..+') - # mapping of instance properties to be transfered to new instance for every + # mapping of instance properties to be transferred to new instance for every # specified family instance_transfer = { "slate": ["slateFrames", "slate"], @@ -398,7 +398,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): continue r_col.indexes.remove(frame) - # now we need to translate published names from represenation + # now we need to translate published names from representation # back. This is tricky, right now we'll just use same naming # and only switch frame numbers resource_files = [] @@ -535,7 +535,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): if preview: new_instance["review"] = True - # create represenation + # create representation if isinstance(col, (list, tuple)): files = [os.path.basename(f) for f in col] else: @@ -748,7 +748,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): # type: (pyblish.api.Instance) -> None """Process plugin. - Detect type of renderfarm submission and create and post dependend job + Detect type of renderfarm submission and create and post dependent job in case of Deadline. It creates json file with metadata needed for publishing in directory of render. @@ -986,7 +986,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): instances = [instance_skeleton_data] # if we are attaching to other subsets, create copy of existing - # instances, change data to match thats subset and replace + # instances, change data to match its subset and replace # existing instances with modified data if instance.data.get("attachTo"): self.log.info("Attaching render to subset:") diff --git a/openpype/modules/ftrack/event_handlers_server/action_clone_review_session.py b/openpype/modules/ftrack/event_handlers_server/action_clone_review_session.py index 1ad7a17785..333228c699 100644 --- a/openpype/modules/ftrack/event_handlers_server/action_clone_review_session.py +++ b/openpype/modules/ftrack/event_handlers_server/action_clone_review_session.py @@ -44,7 +44,7 @@ def clone_review_session(session, entity): class CloneReviewSession(ServerAction): '''Generate Client Review action - `label` a descriptive string identifing your action. + `label` a descriptive string identifying your action. `varaint` To group actions together, give them the same label and specify a unique variant per action. `identifier` a unique identifier for your action. diff --git a/openpype/modules/ftrack/event_handlers_server/action_create_review_session.py b/openpype/modules/ftrack/event_handlers_server/action_create_review_session.py index 21382007a0..42a279e333 100644 --- a/openpype/modules/ftrack/event_handlers_server/action_create_review_session.py +++ b/openpype/modules/ftrack/event_handlers_server/action_create_review_session.py @@ -230,7 +230,7 @@ class CreateDailyReviewSessionServerAction(ServerAction): if not today_session_name: continue - # Find matchin review session + # Find matching review session project_review_sessions = review_sessions_by_project_id[project_id] todays_session = None yesterdays_session = None diff --git a/openpype/modules/ftrack/event_handlers_server/action_prepare_project.py b/openpype/modules/ftrack/event_handlers_server/action_prepare_project.py index 332648cd02..02231cbe3c 100644 --- a/openpype/modules/ftrack/event_handlers_server/action_prepare_project.py +++ b/openpype/modules/ftrack/event_handlers_server/action_prepare_project.py @@ -124,7 +124,7 @@ class PrepareProjectServer(ServerAction): root_items.append({ "type": "label", "value": ( - "

    NOTE: Roots are crutial for path filling" + "

    NOTE: Roots are crucial for path filling" " (and creating folder structure).

    " ) }) diff --git a/openpype/modules/ftrack/event_handlers_server/action_tranfer_hierarchical_values.py b/openpype/modules/ftrack/event_handlers_server/action_tranfer_hierarchical_values.py index d160b7200d..f6899843a3 100644 --- a/openpype/modules/ftrack/event_handlers_server/action_tranfer_hierarchical_values.py +++ b/openpype/modules/ftrack/event_handlers_server/action_tranfer_hierarchical_values.py @@ -12,7 +12,7 @@ from openpype_modules.ftrack.lib.avalon_sync import create_chunks class TransferHierarchicalValues(ServerAction): - """Transfer values across hierarhcical attributes. + """Transfer values across hierarchical attributes. Aalso gives ability to convert types meanwhile. That is limited to conversions between numbers and strings @@ -67,7 +67,7 @@ class TransferHierarchicalValues(ServerAction): "type": "label", "value": ( "Didn't found custom attributes" - " that can be transfered." + " that can be transferred." ) }] } diff --git a/openpype/modules/ftrack/event_handlers_server/event_next_task_update.py b/openpype/modules/ftrack/event_handlers_server/event_next_task_update.py index a65ae46545..a100c34f67 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_next_task_update.py +++ b/openpype/modules/ftrack/event_handlers_server/event_next_task_update.py @@ -279,7 +279,7 @@ class NextTaskUpdate(BaseEvent): except Exception: session.rollback() self.log.warning( - "\"{}\" status couldnt be set to \"{}\"".format( + "\"{}\" status couldn't be set to \"{}\"".format( ent_path, new_status["name"] ), exc_info=True diff --git a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py index 0f10145c06..ed630ad59d 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py +++ b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py @@ -394,7 +394,7 @@ class PushHierValuesToNonHierEvent(BaseEvent): project_id: str, entities_info: list[dict[str, Any]] ): - """Proces changes in single project. + """Process changes in single project. Args: session (ftrack_api.Session): Ftrack session. diff --git a/openpype/modules/ftrack/event_handlers_server/event_radio_buttons.py b/openpype/modules/ftrack/event_handlers_server/event_radio_buttons.py index 99ad3aec37..358a8d2310 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_radio_buttons.py +++ b/openpype/modules/ftrack/event_handlers_server/event_radio_buttons.py @@ -7,7 +7,7 @@ class RadioButtons(BaseEvent): ignore_me = True def launch(self, session, event): - '''Provides a readio button behaviour to any bolean attribute in + '''Provides a radio button behaviour to any boolean attribute in radio_button group.''' # start of event procedure ---------------------------------- diff --git a/openpype/modules/ftrack/event_handlers_server/event_sync_to_avalon.py b/openpype/modules/ftrack/event_handlers_server/event_sync_to_avalon.py index 0058a428e3..0aa0b9f9f5 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_sync_to_avalon.py +++ b/openpype/modules/ftrack/event_handlers_server/event_sync_to_avalon.py @@ -787,7 +787,7 @@ class SyncToAvalonEvent(BaseEvent): # Filter updates where name is changing for ftrack_id, ent_info in updated.items(): ent_keys = ent_info["keys"] - # Seprate update info from rename + # Separate update info from rename if "name" not in ent_keys: continue @@ -827,7 +827,7 @@ class SyncToAvalonEvent(BaseEvent): # 5.) Process updated self.process_updated() time_6 = time.time() - # 6.) Process changes in hierarchy or hier custom attribues + # 6.) Process changes in hierarchy or hier custom attributes self.process_hier_cleanup() time_7 = time.time() self.process_task_updates() @@ -1094,7 +1094,7 @@ class SyncToAvalonEvent(BaseEvent): def check_names_synchronizable(self, names): """Check if entities with specific names are importable. - This check should happend after removing entity or renaming entity. + This check should happen after removing entity or renaming entity. When entity was removed or renamed then it's name is possible to sync. """ joined_passed_names = ", ".join( @@ -1743,7 +1743,7 @@ class SyncToAvalonEvent(BaseEvent): def process_moved(self): """ - Handles moved entities to different place in hiearchy. + Handles moved entities to different place in hierarchy. (Not tasks - handled separately.) """ if not self.ftrack_moved: @@ -1792,7 +1792,7 @@ class SyncToAvalonEvent(BaseEvent): self.log.warning("{} <{}>".format(error_msg, ent_path)) continue - # THIS MUST HAPPEND AFTER CREATING NEW ENTITIES !!!! + # THIS MUST HAPPEN AFTER CREATING NEW ENTITIES !!!! # - because may be moved to new created entity if "data" not in self.updates[mongo_id]: self.updates[mongo_id]["data"] = {} @@ -2323,7 +2323,7 @@ class SyncToAvalonEvent(BaseEvent): items.append("{} - \"{}\"".format(ent_path, value)) self.report_items["error"][fps_msg] = items - # Get dictionary with not None hierarchical values to pull to childs + # Get dictionary with not None hierarchical values to pull to children project_values = {} for key, value in ( entities_dict[ftrack_project_id]["hier_attrs"].items() @@ -2460,7 +2460,7 @@ class SyncToAvalonEvent(BaseEvent): def update_entities(self): """ Update Avalon entities by mongo bulk changes. - Expects self.updates which are transfered to $set part of update + Expects self.updates which are transferred to $set part of update command. Resets self.updates afterwards. """ diff --git a/openpype/modules/ftrack/event_handlers_server/event_task_to_parent_status.py b/openpype/modules/ftrack/event_handlers_server/event_task_to_parent_status.py index a0e039926e..25fa3b0535 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_task_to_parent_status.py +++ b/openpype/modules/ftrack/event_handlers_server/event_task_to_parent_status.py @@ -291,7 +291,7 @@ class TaskStatusToParent(BaseEvent): except Exception: session.rollback() self.log.warning( - "\"{}\" status couldnt be set to \"{}\"".format( + "\"{}\" status couldn't be set to \"{}\"".format( ent_path, new_status["name"] ), exc_info=True @@ -399,7 +399,7 @@ class TaskStatusToParent(BaseEvent): # For cases there are multiple tasks in changes # - task status which match any new status item by order in the - # list `single_match` is preffered + # list `single_match` is preferred best_order = len(single_match) best_order_status = None for task_entity in task_entities: diff --git a/openpype/modules/ftrack/event_handlers_server/event_user_assigment.py b/openpype/modules/ftrack/event_handlers_server/event_user_assigment.py index c4e48b92f0..9539a34f5e 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_user_assigment.py +++ b/openpype/modules/ftrack/event_handlers_server/event_user_assigment.py @@ -10,11 +10,11 @@ from openpype_modules.ftrack.lib.avalon_sync import CUST_ATTR_ID_KEY class UserAssigmentEvent(BaseEvent): """ - This script will intercept user assigment / de-assigment event and + This script will intercept user assignment / de-assignment event and run shell script, providing as much context as possible. It expects configuration file ``presets/ftrack/user_assigment_event.json``. - In it, you define paths to scripts to be run for user assigment event and + In it, you define paths to scripts to be run for user assignment event and for user-deassigment:: { "add": [ diff --git a/openpype/modules/ftrack/event_handlers_server/event_version_to_task_statuses.py b/openpype/modules/ftrack/event_handlers_server/event_version_to_task_statuses.py index e36c3eecd9..fb40fd6417 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_version_to_task_statuses.py +++ b/openpype/modules/ftrack/event_handlers_server/event_version_to_task_statuses.py @@ -102,7 +102,7 @@ class VersionToTaskStatus(BaseEvent): asset_version_entities.append(asset_version) task_ids.add(asset_version["task_id"]) - # Skipt if `task_ids` are empty + # Skip if `task_ids` are empty if not task_ids: return diff --git a/openpype/modules/ftrack/event_handlers_user/action_batch_task_creation.py b/openpype/modules/ftrack/event_handlers_user/action_batch_task_creation.py index c7fb1af98b..06d572601d 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_batch_task_creation.py +++ b/openpype/modules/ftrack/event_handlers_user/action_batch_task_creation.py @@ -7,7 +7,7 @@ from openpype_modules.ftrack.lib import BaseAction, statics_icon class BatchTasksAction(BaseAction): '''Batch Tasks action - `label` a descriptive string identifing your action. + `label` a descriptive string identifying your action. `varaint` To group actions together, give them the same label and specify a unique variant per action. `identifier` a unique identifier for your action. diff --git a/openpype/modules/ftrack/event_handlers_user/action_create_cust_attrs.py b/openpype/modules/ftrack/event_handlers_user/action_create_cust_attrs.py index c19cfd1502..471a8c4182 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_create_cust_attrs.py +++ b/openpype/modules/ftrack/event_handlers_user/action_create_cust_attrs.py @@ -82,9 +82,9 @@ config (dictionary) write_security_roles/read_security_roles (array of strings) - default: ["ALL"] - strings should be role names (e.g.: ["API", "Administrator"]) - - if set to ["ALL"] - all roles will be availabled + - if set to ["ALL"] - all roles will be available - if first is 'except' - roles will be set to all except roles in array - - Warning: Be carefull with except - roles can be different by company + - Warning: Be careful with except - roles can be different by company - example: write_security_roles = ["except", "User"] read_security_roles = ["ALL"] # (User is can only read) @@ -500,7 +500,7 @@ class CustomAttributes(BaseAction): data = {} # Get key, label, type data.update(self.get_required(cust_attr_data)) - # Get hierachical/ entity_type/ object_id + # Get hierarchical/ entity_type/ object_id data.update(self.get_entity_type(cust_attr_data)) # Get group, default, security roles data.update(self.get_optional(cust_attr_data)) diff --git a/openpype/modules/ftrack/event_handlers_user/action_create_folders.py b/openpype/modules/ftrack/event_handlers_user/action_create_folders.py index 9806f83773..cbeff5343f 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_create_folders.py +++ b/openpype/modules/ftrack/event_handlers_user/action_create_folders.py @@ -51,7 +51,7 @@ class CreateFolders(BaseAction): }, { "type": "label", - "value": "With all chilren entities" + "value": "With all children entities" }, { "name": "children_included", diff --git a/openpype/modules/ftrack/event_handlers_user/action_delete_asset.py b/openpype/modules/ftrack/event_handlers_user/action_delete_asset.py index 03d029b0c1..72a5efbcfe 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_delete_asset.py +++ b/openpype/modules/ftrack/event_handlers_user/action_delete_asset.py @@ -18,7 +18,7 @@ class DeleteAssetSubset(BaseAction): # Action label. label = "Delete Asset/Subsets" # Action description. - description = "Removes from Avalon with all childs and asset from Ftrack" + description = "Removes from Avalon with all children and asset from Ftrack" icon = statics_icon("ftrack", "action_icons", "DeleteAsset.svg") settings_key = "delete_asset_subset" diff --git a/openpype/modules/ftrack/event_handlers_user/action_delete_old_versions.py b/openpype/modules/ftrack/event_handlers_user/action_delete_old_versions.py index c543dc8834..ec14c6918b 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_delete_old_versions.py +++ b/openpype/modules/ftrack/event_handlers_user/action_delete_old_versions.py @@ -27,7 +27,7 @@ class DeleteOldVersions(BaseAction): variant = "- Delete old versions" description = ( "Delete files from older publishes so project can be" - " archived with only lates versions." + " archived with only latest versions." ) icon = statics_icon("ftrack", "action_icons", "OpenPypeAdmin.svg") @@ -307,7 +307,7 @@ class DeleteOldVersions(BaseAction): file_path, seq_path = self.path_from_represenation(repre, anatomy) if file_path is None: self.log.warning(( - "Could not format path for represenation \"{}\"" + "Could not format path for representation \"{}\"" ).format(str(repre))) continue diff --git a/openpype/modules/ftrack/event_handlers_user/action_delivery.py b/openpype/modules/ftrack/event_handlers_user/action_delivery.py index a400c8f5f0..559de3a24d 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_delivery.py +++ b/openpype/modules/ftrack/event_handlers_user/action_delivery.py @@ -601,7 +601,7 @@ class Delivery(BaseAction): return self.report(report_items) def report(self, report_items): - """Returns dict with final status of delivery (succes, fail etc.).""" + """Returns dict with final status of delivery (success, fail etc.).""" items = [] for msg, _items in report_items.items(): diff --git a/openpype/modules/ftrack/event_handlers_user/action_fill_workfile_attr.py b/openpype/modules/ftrack/event_handlers_user/action_fill_workfile_attr.py index fb1cdf340e..36d29db96b 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_fill_workfile_attr.py +++ b/openpype/modules/ftrack/event_handlers_user/action_fill_workfile_attr.py @@ -246,7 +246,7 @@ class FillWorkfileAttributeAction(BaseAction): project_name = project_entity["full_name"] - # Find matchin asset documents and map them by ftrack task entities + # Find matching asset documents and map them by ftrack task entities # - result stored to 'asset_docs_with_task_entities' is list with # tuple `(asset document, [task entitis, ...])` # Quety all asset documents diff --git a/openpype/modules/ftrack/event_handlers_user/action_job_killer.py b/openpype/modules/ftrack/event_handlers_user/action_job_killer.py index f489c0c54c..dd68c75f84 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_job_killer.py +++ b/openpype/modules/ftrack/event_handlers_user/action_job_killer.py @@ -54,14 +54,14 @@ class JobKiller(BaseAction): for job in jobs: try: data = json.loads(job["data"]) - desctiption = data["description"] + description = data["description"] except Exception: - desctiption = "*No description*" + description = "*No description*" user_id = job["user_id"] username = usernames_by_id.get(user_id) or "Unknown user" created = job["created_at"].strftime('%d.%m.%Y %H:%M:%S') label = "{} - {} - {}".format( - username, desctiption, created + username, description, created ) item_label = { "type": "label", diff --git a/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py b/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py index e825198180..19d5701e08 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py +++ b/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py @@ -24,7 +24,7 @@ class PrepareProjectLocal(BaseAction): settings_key = "prepare_project" - # Key to store info about trigerring create folder structure + # Key to store info about triggering create folder structure create_project_structure_key = "create_folder_structure" create_project_structure_identifier = "create.project.structure" item_splitter = {"type": "label", "value": "---"} @@ -146,7 +146,7 @@ class PrepareProjectLocal(BaseAction): root_items.append({ "type": "label", "value": ( - "

    NOTE: Roots are crutial for path filling" + "

    NOTE: Roots are crucial for path filling" " (and creating folder structure).

    " ) }) diff --git a/openpype/modules/ftrack/event_handlers_user/action_rv.py b/openpype/modules/ftrack/event_handlers_user/action_rv.py index d05f0c47f6..39cf33d605 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_rv.py +++ b/openpype/modules/ftrack/event_handlers_user/action_rv.py @@ -66,7 +66,7 @@ class RVAction(BaseAction): def get_components_from_entity(self, session, entity, components): """Get components from various entity types. - The components dictionary is modifid in place, so nothing is returned. + The components dictionary is modified in place, so nothing is returned. Args: entity (Ftrack entity) diff --git a/openpype/modules/ftrack/event_handlers_user/action_seed.py b/openpype/modules/ftrack/event_handlers_user/action_seed.py index 4021d70c0a..657cd07a9f 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_seed.py +++ b/openpype/modules/ftrack/event_handlers_user/action_seed.py @@ -325,8 +325,8 @@ class SeedDebugProject(BaseAction): ): index = 0 - self.log.debug("*** Commiting Assets") - self.log.debug("Commiting entities. {}/{}".format( + self.log.debug("*** Committing Assets") + self.log.debug("Committing entities. {}/{}".format( created_entities, to_create_length )) self.session.commit() @@ -414,8 +414,8 @@ class SeedDebugProject(BaseAction): ): index = 0 - self.log.debug("*** Commiting Shots") - self.log.debug("Commiting entities. {}/{}".format( + self.log.debug("*** Committing Shots") + self.log.debug("Committing entities. {}/{}".format( created_entities, to_create_length )) self.session.commit() @@ -423,7 +423,7 @@ class SeedDebugProject(BaseAction): def temp_commit(self, index, created_entities, to_create_length): if index < self.max_entities_created_at_one_commit: return False - self.log.debug("Commiting {} entities. {}/{}".format( + self.log.debug("Committing {} entities. {}/{}".format( index, created_entities, to_create_length )) self.session.commit() diff --git a/openpype/modules/ftrack/event_handlers_user/action_store_thumbnails_to_avalon.py b/openpype/modules/ftrack/event_handlers_user/action_store_thumbnails_to_avalon.py index 8748f426bd..c9e0901623 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_store_thumbnails_to_avalon.py +++ b/openpype/modules/ftrack/event_handlers_user/action_store_thumbnails_to_avalon.py @@ -184,7 +184,7 @@ class StoreThumbnailsToAvalon(BaseAction): self.db_con.install() for entity in entities: - # Skip if entity is not AssetVersion (never should happend, but..) + # Skip if entity is not AssetVersion (should never happen, but..) if entity.entity_type.lower() != "assetversion": continue diff --git a/openpype/modules/ftrack/ftrack_server/event_server_cli.py b/openpype/modules/ftrack/ftrack_server/event_server_cli.py index ad7ffd8e25..77f479ee20 100644 --- a/openpype/modules/ftrack/ftrack_server/event_server_cli.py +++ b/openpype/modules/ftrack/ftrack_server/event_server_cli.py @@ -33,7 +33,7 @@ class MongoPermissionsError(Exception): """Is used when is created multiple objects of same RestApi class.""" def __init__(self, message=None): if not message: - message = "Exiting because have issue with acces to MongoDB" + message = "Exiting because have issue with access to MongoDB" super().__init__(message) @@ -340,7 +340,7 @@ def main_loop(ftrack_url): return 1 # ====== STORER ======= - # Run backup thread which does not requeire mongo to work + # Run backup thread which does not require mongo to work if storer_thread is None: if storer_failed_count < max_fail_count: storer_thread = socket_thread.SocketThread( @@ -399,7 +399,7 @@ def main_loop(ftrack_url): elif not processor_thread.is_alive(): if processor_thread.mongo_error: raise Exception( - "Exiting because have issue with acces to MongoDB" + "Exiting because have issue with access to MongoDB" ) processor_thread.join() processor_thread = None diff --git a/openpype/modules/ftrack/lib/avalon_sync.py b/openpype/modules/ftrack/lib/avalon_sync.py index 0341c25717..08dcf2e671 100644 --- a/openpype/modules/ftrack/lib/avalon_sync.py +++ b/openpype/modules/ftrack/lib/avalon_sync.py @@ -891,7 +891,7 @@ class SyncEntitiesFactory: parent_dict = self.entities_dict.get(parent_id, {}) for child_id in parent_dict.get("children", []): - # keep original `remove` value for all childs + # keep original `remove` value for all children _remove = (remove is True) if not _remove: if self.entities_dict[child_id]["avalon_attrs"].get( @@ -1191,8 +1191,8 @@ class SyncEntitiesFactory: avalon_hier = [] for item in items: value = item["value"] - # WARNING It is not possible to propage enumerate hierachical - # attributes with multiselection 100% right. Unseting all values + # WARNING It is not possible to propagate enumerate hierarchical + # attributes with multiselection 100% right. Unsetting all values # will cause inheritance from parent. if ( value is None @@ -1231,7 +1231,7 @@ class SyncEntitiesFactory: items.append("{} - \"{}\"".format(ent_path, value)) self.report_items["error"][fps_msg] = items - # Get dictionary with not None hierarchical values to pull to childs + # Get dictionary with not None hierarchical values to pull to children top_id = self.ft_project_id project_values = {} for key, value in self.entities_dict[top_id]["hier_attrs"].items(): @@ -1749,7 +1749,7 @@ class SyncEntitiesFactory: # TODO logging ent_path = self.get_ent_path(ftrack_id) msg = ( - " It is not possible" + " It is not possible" " to change the hierarchy of an entity or it's parents," " if it already contained published data." ) @@ -2584,7 +2584,7 @@ class SyncEntitiesFactory: # # ent_dict = self.entities_dict[found_by_name_id] - # TODO report - CRITICAL entity with same name alread exists in + # TODO report - CRITICAL entity with same name already exists in # different hierarchy - can't recreate entity continue diff --git a/openpype/modules/ftrack/lib/custom_attributes.py b/openpype/modules/ftrack/lib/custom_attributes.py index 2f53815368..3e40bb02f2 100644 --- a/openpype/modules/ftrack/lib/custom_attributes.py +++ b/openpype/modules/ftrack/lib/custom_attributes.py @@ -65,7 +65,7 @@ def get_openpype_attr(session, split_hierarchical=True, query_keys=None): cust_attrs_query = ( "select {}" " from CustomAttributeConfiguration" - # Kept `pype` for Backwards Compatiblity + # Kept `pype` for Backwards Compatibility " where group.name in (\"pype\", \"{}\")" ).format(", ".join(query_keys), CUST_ATTR_GROUP) all_avalon_attr = session.query(cust_attrs_query).all() diff --git a/openpype/modules/ftrack/lib/ftrack_action_handler.py b/openpype/modules/ftrack/lib/ftrack_action_handler.py index b24fe5f12a..07b3a780a2 100644 --- a/openpype/modules/ftrack/lib/ftrack_action_handler.py +++ b/openpype/modules/ftrack/lib/ftrack_action_handler.py @@ -12,7 +12,7 @@ def statics_icon(*icon_statics_file_parts): class BaseAction(BaseHandler): '''Custom Action base class - `label` a descriptive string identifing your action. + `label` a descriptive string identifying your action. `varaint` To group actions together, give them the same label and specify a unique variant per action. diff --git a/openpype/modules/ftrack/lib/ftrack_base_handler.py b/openpype/modules/ftrack/lib/ftrack_base_handler.py index c0b03f8a41..55400c22ab 100644 --- a/openpype/modules/ftrack/lib/ftrack_base_handler.py +++ b/openpype/modules/ftrack/lib/ftrack_base_handler.py @@ -30,7 +30,7 @@ class PreregisterException(Exception): class BaseHandler(object): '''Custom Action base class -