From c104805830c349260b30756b19560836bd9866a6 Mon Sep 17 00:00:00 2001 From: moonyuet Date: Tue, 14 Mar 2023 16:17:17 +0100 Subject: [PATCH 001/147] adding creator, loader for redshift proxy in 3dsmax --- .../plugins/create/create_redshift_proxy.py | 26 +++++++ .../max/plugins/load/load_redshift_proxy.py | 59 ++++++++++++++ .../plugins/publish/extract_redshift_proxy.py | 78 +++++++++++++++++++ 3 files changed, 163 insertions(+) create mode 100644 openpype/hosts/max/plugins/create/create_redshift_proxy.py create mode 100644 openpype/hosts/max/plugins/load/load_redshift_proxy.py create mode 100644 openpype/hosts/max/plugins/publish/extract_redshift_proxy.py diff --git a/openpype/hosts/max/plugins/create/create_redshift_proxy.py b/openpype/hosts/max/plugins/create/create_redshift_proxy.py new file mode 100644 index 0000000000..83ddc3a193 --- /dev/null +++ b/openpype/hosts/max/plugins/create/create_redshift_proxy.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +"""Creator plugin for creating camera.""" +from openpype.hosts.max.api import plugin +from openpype.pipeline import CreatedInstance + + +class CreateRedshiftProxy(plugin.MaxCreator): + identifier = "io.openpype.creators.max.redshiftproxy" + label = "Redshift Proxy" + family = "redshiftproxy" + icon = "gear" + + def create(self, subset_name, instance_data, pre_create_data): + from pymxs import runtime as rt + sel_obj = list(rt.selection) + instance = super(CreateRedshiftProxy, self).create( + subset_name, + instance_data, + pre_create_data) # type: CreatedInstance + container = rt.getNodeByName(instance.data.get("instance_node")) + # TODO: Disable "Add to Containers?" Panel + # parent the selected cameras into the container + for obj in sel_obj: + obj.parent = container + # for additional work on the node: + # instance_node = rt.getNodeByName(instance.get("instance_node")) diff --git a/openpype/hosts/max/plugins/load/load_redshift_proxy.py b/openpype/hosts/max/plugins/load/load_redshift_proxy.py new file mode 100644 index 0000000000..7a5e94158f --- /dev/null +++ b/openpype/hosts/max/plugins/load/load_redshift_proxy.py @@ -0,0 +1,59 @@ +import os +import clique + +from openpype.pipeline import ( + load, + get_representation_path +) +from openpype.hosts.max.api.pipeline import containerise +from openpype.hosts.max.api import lib + + +class RedshiftProxyLoader(load.LoaderPlugin): + """Redshift Proxy Loader""" + + families = ["redshiftproxy"] + representations = ["rs"] + order = -9 + icon = "code-fork" + color = "white" + + def load(self, context, name=None, namespace=None, data=None): + from pymxs import runtime as rt + + filepath = os.path.normpath(self.fname) + rs_proxy = rt.RedshiftProxy() + rs_proxy.file = filepath + files_in_folder = os.listdir(os.path.dirname(filepath)) + collections, remainder = clique.assemble(files_in_folder) + if collections: + rs_proxy.is_sequence = True + + container = rt.container() + container.name = f"{name}" + rs_proxy.Parent = container + + asset = rt.getNodeByName(f"{name}") + + return containerise( + name, [asset], context, loader=self.__class__.__name__) + + def update(self, container, representation): + from pymxs import runtime as rt + + path = get_representation_path(representation) + node = rt.getNodeByName(container["instance_node"]) + + proxy_objects = self.get_container_children(node) + for proxy in proxy_objects: + proxy.source = path + + lib.imprint(container["instance_node"], { + "representation": str(representation["_id"]) + }) + + def remove(self, container): + from pymxs import runtime as rt + + node = rt.getNodeByName(container["instance_node"]) + rt.delete(node) diff --git a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py new file mode 100644 index 0000000000..f9dd726ef4 --- /dev/null +++ b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py @@ -0,0 +1,78 @@ +import os +import pyblish.api +from openpype.pipeline import ( + publish, + OptionalPyblishPluginMixin +) +from pymxs import runtime as rt +from openpype.hosts.max.api import ( + maintained_selection, + get_all_children +) + + +class ExtractCameraAlembic(publish.Extractor, + OptionalPyblishPluginMixin): + """ + Extract Camera with AlembicExport + """ + + order = pyblish.api.ExtractorOrder - 0.1 + label = "Extract RedShift Proxy" + hosts = ["max"] + families = ["redshiftproxy"] + + def process(self, instance): + container = instance.data["instance_node"] + start = int(instance.context.data.get("frameStart")) + end = int(instance.context.data.get("frameEnd")) + + self.log.info("Extracting Redshift Proxy...") + stagingdir = self.staging_dir(instance) + rs_filename = "{name}.rs".format(**instance.data) + + rs_filepath = os.path.join(stagingdir, rs_filename) + + # MaxScript command for export + export_cmd = ( + f""" +fn ProxyExport fp selected:true compress:false connectivity:false startFrame: endFrame: camera:undefined warnExisting:true transformPivotToOrigin:false = ( + if startFrame == unsupplied then ( + startFrame = (currentTime.frame as integer) + ) + + if endFrame == unsupplied then ( + endFrame = (currentTime.frame as integer) + ) + + ret = rsProxy fp selected compress connectivity startFrame endFrame camera warnExisting transformPivotToOrigin + + ret +) +execute = ProxyExport fp selected:true compress:false connectivity:false startFrame:{start} endFrame:{end} warnExisting:false transformPivotToOrigin:bTransformPivotToOrigin + + """) # noqa + + with maintained_selection(): + # select and export + rt.select(container.Children) + rt.execute(export_cmd) + + self.log.info("Performing Extraction ...") + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': 'rs', + 'ext': 'rs', + # need to count the files + 'files': rs_filename, + "stagingDir": stagingdir, + } + instance.data["representations"].append(representation) + self.log.info("Extracted instance '%s' to: %s" % (instance.name, + rs_filepath)) + + # TODO: set sequence + def get_rsfiles(self, container, startFrame, endFrame): + pass From 5af9867dedda3eb154fedd4aba0c93d8438b80df Mon Sep 17 00:00:00 2001 From: moonyuet Date: Tue, 14 Mar 2023 16:25:00 +0100 Subject: [PATCH 002/147] update fix --- openpype/hosts/max/plugins/publish/extract_redshift_proxy.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py index f9dd726ef4..85d249b020 100644 --- a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py +++ b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py @@ -6,8 +6,7 @@ from openpype.pipeline import ( ) from pymxs import runtime as rt from openpype.hosts.max.api import ( - maintained_selection, - get_all_children + maintained_selection ) From 9cbac449fded786bc033931a07c9e44dd18907e2 Mon Sep 17 00:00:00 2001 From: moonyuet Date: Tue, 14 Mar 2023 16:26:06 +0100 Subject: [PATCH 003/147] change the name --- openpype/hosts/max/plugins/publish/extract_redshift_proxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py index 85d249b020..938a7e8c2c 100644 --- a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py +++ b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py @@ -10,7 +10,7 @@ from openpype.hosts.max.api import ( ) -class ExtractCameraAlembic(publish.Extractor, +class ExtractRedshiftProxy(publish.Extractor, OptionalPyblishPluginMixin): """ Extract Camera with AlembicExport From 119bb1a586548e6997f3e2724660c19e0d346a56 Mon Sep 17 00:00:00 2001 From: moonyuet Date: Tue, 14 Mar 2023 16:37:25 +0100 Subject: [PATCH 004/147] update the loader and creator --- openpype/hosts/max/plugins/create/create_redshift_proxy.py | 5 +---- openpype/hosts/max/plugins/load/load_redshift_proxy.py | 6 +++--- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/max/plugins/create/create_redshift_proxy.py b/openpype/hosts/max/plugins/create/create_redshift_proxy.py index 83ddc3a193..ca0891fc5b 100644 --- a/openpype/hosts/max/plugins/create/create_redshift_proxy.py +++ b/openpype/hosts/max/plugins/create/create_redshift_proxy.py @@ -18,9 +18,6 @@ class CreateRedshiftProxy(plugin.MaxCreator): instance_data, pre_create_data) # type: CreatedInstance container = rt.getNodeByName(instance.data.get("instance_node")) - # TODO: Disable "Add to Containers?" Panel - # parent the selected cameras into the container + for obj in sel_obj: obj.parent = container - # for additional work on the node: - # instance_node = rt.getNodeByName(instance.get("instance_node")) diff --git a/openpype/hosts/max/plugins/load/load_redshift_proxy.py b/openpype/hosts/max/plugins/load/load_redshift_proxy.py index 7a5e94158f..13003d764a 100644 --- a/openpype/hosts/max/plugins/load/load_redshift_proxy.py +++ b/openpype/hosts/max/plugins/load/load_redshift_proxy.py @@ -10,8 +10,8 @@ from openpype.hosts.max.api import lib class RedshiftProxyLoader(load.LoaderPlugin): - """Redshift Proxy Loader""" + label = "Load Redshift Proxy" families = ["redshiftproxy"] representations = ["rs"] order = -9 @@ -21,7 +21,7 @@ class RedshiftProxyLoader(load.LoaderPlugin): def load(self, context, name=None, namespace=None, data=None): from pymxs import runtime as rt - filepath = os.path.normpath(self.fname) + filepath = self.filepath_from_context(context) rs_proxy = rt.RedshiftProxy() rs_proxy.file = filepath files_in_folder = os.listdir(os.path.dirname(filepath)) @@ -30,7 +30,7 @@ class RedshiftProxyLoader(load.LoaderPlugin): rs_proxy.is_sequence = True container = rt.container() - container.name = f"{name}" + container.name = name rs_proxy.Parent = container asset = rt.getNodeByName(f"{name}") From f3bd329d5a40793a6e083198326b22b43a58c621 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 28 Mar 2023 18:48:58 +0800 Subject: [PATCH 005/147] add validator for checking if the current renderer is redshift before the extraction --- .../validate_renderer_redshift_proxy.py | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py diff --git a/openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py b/openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py new file mode 100644 index 0000000000..3a921c386e --- /dev/null +++ b/openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +import pyblish.api +from openpype.pipeline import PublishValidationError +from pymxs import runtime as rt +from openpype.pipeline.publish import RepairAction +from openpype.hosts.max.api.lib import get_current_renderer + + +class ValidateRendererRedshiftProxy(pyblish.api.InstancePlugin): + """ + Validates Redshift as the current renderer for creating + Redshift Proxy + """ + + order = pyblish.api.ValidatorOrder + families = ["redshiftproxy"] + hosts = ["max"] + label = "Redshift Renderer" + actions = [RepairAction] + + def process(self, instance): + invalid = self.get_all_renderer(instance) + if invalid: + raise PublishValidationError("Please install Redshift for 3dsMax" + " before using this!") + invalid = self.get_current_renderer(instance) + if invalid: + raise PublishValidationError("Current Renderer is not Redshift") + + def get_all_renderer(self, instance): + invalid = list() + max_renderers_list = str(rt.RendererClass.classes) + if "Redshift_Renderer" not in max_renderers_list: + invalid.append(max_renderers_list) + + return invalid + + def get_current_renderer(self, instance): + invalid = list() + renderer_class = get_current_renderer() + current_renderer = str(renderer_class).split(":")[0] + if current_renderer != "Redshift_Renderer": + invalid.append(current_renderer) + + return invalid + + @classmethod + def repair(cls, instance): + if "Redshift_Renderer" in str(rt.RendererClass.classes[2]()): + rt.renderers.production = rt.RendererClass.classes[2]() From 91abe54b01ce08856004b7e395735c9508ab8300 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 28 Mar 2023 22:35:02 +0800 Subject: [PATCH 006/147] add the extractor for redshift proxy --- .../plugins/publish/extract_redshift_proxy.py | 48 ++++++++----------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py index 938a7e8c2c..1616ead0ac 100644 --- a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py +++ b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py @@ -6,7 +6,8 @@ from openpype.pipeline import ( ) from pymxs import runtime as rt from openpype.hosts.max.api import ( - maintained_selection + maintained_selection, + get_all_children ) @@ -29,35 +30,22 @@ class ExtractRedshiftProxy(publish.Extractor, self.log.info("Extracting Redshift Proxy...") stagingdir = self.staging_dir(instance) rs_filename = "{name}.rs".format(**instance.data) - rs_filepath = os.path.join(stagingdir, rs_filename) + rs_filepath = rs_filepath.replace("\\", "/") - # MaxScript command for export - export_cmd = ( - f""" -fn ProxyExport fp selected:true compress:false connectivity:false startFrame: endFrame: camera:undefined warnExisting:true transformPivotToOrigin:false = ( - if startFrame == unsupplied then ( - startFrame = (currentTime.frame as integer) - ) - - if endFrame == unsupplied then ( - endFrame = (currentTime.frame as integer) - ) - - ret = rsProxy fp selected compress connectivity startFrame endFrame camera warnExisting transformPivotToOrigin - - ret -) -execute = ProxyExport fp selected:true compress:false connectivity:false startFrame:{start} endFrame:{end} warnExisting:false transformPivotToOrigin:bTransformPivotToOrigin - - """) # noqa + rs_filenames = self.get_rsfiles(instance, start, end) with maintained_selection(): # select and export - rt.select(container.Children) - rt.execute(export_cmd) + # con = rt.getNodeByName(container) + rt.select(get_all_children(rt.getNodeByName(container))) + # Redshift rsProxy command + # rsProxy fp selected compress connectivity startFrame endFrame + # camera warnExisting transformPivotToOrigin + rt.rsProxy(rs_filepath, 1, 0, 0, start, end, 0, 1, 1) self.log.info("Performing Extraction ...") + if "representations" not in instance.data: instance.data["representations"] = [] @@ -65,13 +53,19 @@ execute = ProxyExport fp selected:true compress:false connectivity:false startFr 'name': 'rs', 'ext': 'rs', # need to count the files - 'files': rs_filename, + 'files': rs_filenames if len(rs_filenames) > 1 else rs_filenames[0], "stagingDir": stagingdir, } instance.data["representations"].append(representation) self.log.info("Extracted instance '%s' to: %s" % (instance.name, - rs_filepath)) + stagingdir)) # TODO: set sequence - def get_rsfiles(self, container, startFrame, endFrame): - pass + def get_rsfiles(self, instance, startFrame, endFrame): + rs_filenames = [] + rs_name = instance.data["name"] + for frame in range(startFrame, endFrame + 1): + rs_filename = "%s.%04d.rs" % (rs_name, frame) + rs_filenames.append(rs_filename) + + return rs_filenames From be6813293c605d5b477af72fedcca047b6e7f0c0 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 28 Mar 2023 22:36:22 +0800 Subject: [PATCH 007/147] shut hound --- openpype/hosts/max/plugins/publish/extract_redshift_proxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py index 1616ead0ac..8924242a93 100644 --- a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py +++ b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py @@ -53,7 +53,7 @@ class ExtractRedshiftProxy(publish.Extractor, 'name': 'rs', 'ext': 'rs', # need to count the files - 'files': rs_filenames if len(rs_filenames) > 1 else rs_filenames[0], + 'files': rs_filenames if len(rs_filenames) > 1 else rs_filenames[0], # noqa "stagingDir": stagingdir, } instance.data["representations"].append(representation) From f25b5d309ad212b273a2ba6ccdfa6b1d950e5a25 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 28 Mar 2023 22:42:28 +0800 Subject: [PATCH 008/147] cleanup --- .../max/plugins/publish/extract_redshift_proxy.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py index 8924242a93..bf16c8d4a9 100644 --- a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py +++ b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py @@ -1,9 +1,6 @@ import os import pyblish.api -from openpype.pipeline import ( - publish, - OptionalPyblishPluginMixin -) +from openpype.pipeline import publish from pymxs import runtime as rt from openpype.hosts.max.api import ( maintained_selection, @@ -11,10 +8,9 @@ from openpype.hosts.max.api import ( ) -class ExtractRedshiftProxy(publish.Extractor, - OptionalPyblishPluginMixin): +class ExtractRedshiftProxy(publish.Extractor): """ - Extract Camera with AlembicExport + Extract Redshift Proxy """ order = pyblish.api.ExtractorOrder - 0.1 From 6d51333a20e13b07d8f32ea69c299163b1c90fc4 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 28 Mar 2023 22:45:52 +0800 Subject: [PATCH 009/147] add docstrings --- openpype/hosts/max/plugins/publish/extract_redshift_proxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py index bf16c8d4a9..c91391429d 100644 --- a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py +++ b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py @@ -10,7 +10,7 @@ from openpype.hosts.max.api import ( class ExtractRedshiftProxy(publish.Extractor): """ - Extract Redshift Proxy + Extract Redshift Proxy with rsProxy """ order = pyblish.api.ExtractorOrder - 0.1 From b9ec96fdd64b4d8e7f770dfc722e459f28cd596e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 28 Mar 2023 22:48:21 +0800 Subject: [PATCH 010/147] add docstring for the rs loader --- openpype/hosts/max/plugins/load/load_redshift_proxy.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/max/plugins/load/load_redshift_proxy.py b/openpype/hosts/max/plugins/load/load_redshift_proxy.py index 13003d764a..30879bca78 100644 --- a/openpype/hosts/max/plugins/load/load_redshift_proxy.py +++ b/openpype/hosts/max/plugins/load/load_redshift_proxy.py @@ -11,6 +11,8 @@ from openpype.hosts.max.api import lib class RedshiftProxyLoader(load.LoaderPlugin): + """Load rs files with Redshift Proxy""" + label = "Load Redshift Proxy" families = ["redshiftproxy"] representations = ["rs"] From 3ba7b9b1ffc8ba9877024f8175af8adcbd984e08 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 28 Mar 2023 23:05:25 +0800 Subject: [PATCH 011/147] fix selection of children --- openpype/hosts/max/plugins/publish/extract_redshift_proxy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py index c91391429d..5aba257443 100644 --- a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py +++ b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py @@ -33,8 +33,8 @@ class ExtractRedshiftProxy(publish.Extractor): with maintained_selection(): # select and export - # con = rt.getNodeByName(container) - rt.select(get_all_children(rt.getNodeByName(container))) + con = rt.getNodeByName(container) + rt.select(con.Children) # Redshift rsProxy command # rsProxy fp selected compress connectivity startFrame endFrame # camera warnExisting transformPivotToOrigin From 35448073aba93b3ead99513b0d831accb00c76cb Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 28 Mar 2023 23:07:05 +0800 Subject: [PATCH 012/147] hound fix --- openpype/hosts/max/plugins/publish/extract_redshift_proxy.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py index 5aba257443..0a3579d687 100644 --- a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py +++ b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py @@ -2,10 +2,7 @@ import os import pyblish.api from openpype.pipeline import publish from pymxs import runtime as rt -from openpype.hosts.max.api import ( - maintained_selection, - get_all_children -) +from openpype.hosts.max.api import maintained_selection class ExtractRedshiftProxy(publish.Extractor): From 14b8139a5cfe92680233343d8e4120ae2253865f Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 3 Apr 2023 16:47:15 +0800 Subject: [PATCH 013/147] Roy's comment & fix the loader update --- openpype/hosts/max/plugins/load/load_redshift_proxy.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_redshift_proxy.py b/openpype/hosts/max/plugins/load/load_redshift_proxy.py index 30879bca78..fd79a2b97c 100644 --- a/openpype/hosts/max/plugins/load/load_redshift_proxy.py +++ b/openpype/hosts/max/plugins/load/load_redshift_proxy.py @@ -35,7 +35,7 @@ class RedshiftProxyLoader(load.LoaderPlugin): container.name = name rs_proxy.Parent = container - asset = rt.getNodeByName(f"{name}") + asset = rt.getNodeByName(name) return containerise( name, [asset], context, loader=self.__class__.__name__) @@ -45,10 +45,10 @@ class RedshiftProxyLoader(load.LoaderPlugin): path = get_representation_path(representation) node = rt.getNodeByName(container["instance_node"]) - - proxy_objects = self.get_container_children(node) - for proxy in proxy_objects: - proxy.source = path + for children in node.Children: + children_node = rt.getNodeByName(children.name) + for proxy in children_node.Children: + proxy.file = path lib.imprint(container["instance_node"], { "representation": str(representation["_id"]) From 6423479078a7305d9e73f55243eb42796acb7820 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 3 Apr 2023 16:50:33 +0800 Subject: [PATCH 014/147] add switch version in the loader --- openpype/hosts/max/plugins/load/load_redshift_proxy.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/max/plugins/load/load_redshift_proxy.py b/openpype/hosts/max/plugins/load/load_redshift_proxy.py index fd79a2b97c..9451e5299b 100644 --- a/openpype/hosts/max/plugins/load/load_redshift_proxy.py +++ b/openpype/hosts/max/plugins/load/load_redshift_proxy.py @@ -54,6 +54,9 @@ class RedshiftProxyLoader(load.LoaderPlugin): "representation": str(representation["_id"]) }) + def switch(self, container, representation): + self.update(container, representation) + def remove(self, container): from pymxs import runtime as rt From 122a4dc9db074f7bd14421e1d8b9244a05318da7 Mon Sep 17 00:00:00 2001 From: Michael reda Date: Wed, 5 Apr 2023 12:14:06 +0200 Subject: [PATCH 015/147] add sync to specific projects or listen only --- openpype/modules/kitsu/kitsu_module.py | 16 ++++++++++-- .../modules/kitsu/utils/update_op_with_zou.py | 25 +++++++++++++++++-- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/openpype/modules/kitsu/kitsu_module.py b/openpype/modules/kitsu/kitsu_module.py index b91373af20..f4e3dd5691 100644 --- a/openpype/modules/kitsu/kitsu_module.py +++ b/openpype/modules/kitsu/kitsu_module.py @@ -124,19 +124,31 @@ def push_to_zou(login, password): @cli_main.command() +@click.option("-prjs", "--projects", envvar="SYNC_PROJECTS", help="Sync specific kitsu projects") @click.option("-l", "--login", envvar="KITSU_LOGIN", help="Kitsu login") @click.option( "-p", "--password", envvar="KITSU_PWD", help="Password for kitsu username" ) -def sync_service(login, password): +def sync_service(login, password, projects="^"): """Synchronize openpype database from Zou sever database. Args: login (str): Kitsu user login password (str): Kitsu user password + projects (str): specific kitsu projects + + SYNC_PROJECTS: + *: all projects + ^: dont sync any project just listen + "project01 project02 ...": to choose custom projects + + """ from .utils.update_op_with_zou import sync_all_projects from .utils.sync_service import start_listeners - sync_all_projects(login, password) + projects = projects.strip() + projects = projects.split(' ') + + sync_all_projects(login, password, specific_projects=projects) start_listeners(login, password) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index 4f4f0810bc..a397198a13 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -359,7 +359,7 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: def sync_all_projects( - login: str, password: str, ignore_projects: list = None + login: str, password: str, ignore_projects: list = None, specific_projects: list = None ): """Update all OP projects in DB with Zou data. @@ -367,6 +367,7 @@ def sync_all_projects( login (str): Kitsu user login password (str): Kitsu user password ignore_projects (list): List of unsynced project names + specific_projects (list): List of synced project names Raises: gazu.exception.AuthFailedException: Wrong user login and/or password """ @@ -381,7 +382,27 @@ def sync_all_projects( dbcon = AvalonMongoDB() dbcon.install() all_projects = gazu.project.all_projects() - for project in all_projects: + + + project_to_sync = [] + if specific_projects == ['*']: + project_to_sync = all_projects + + elif specific_projects == ['^']: + return + + elif isinstance(specific_projects, list): + all_kitsu_projects = {p['name']: p for p in all_projects} + for proj_name in specific_projects: + if proj_name in all_kitsu_projects: + project_to_sync.append(all_kitsu_projects[proj_name]) + else: + log.info(f'`{proj_name}` project does not exists in kitsu.' + f' Please make sure you write the project correctly.') + else: + return + + for project in project_to_sync: if ignore_projects and project["name"] in ignore_projects: continue sync_project_from_kitsu(dbcon, project) From 442236284bc87cf3a4aff4d3ae622beaaf946c4c Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 6 Apr 2023 17:53:42 +0800 Subject: [PATCH 016/147] add docs --- website/docs/artist_hosts_3dsmax.md | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/website/docs/artist_hosts_3dsmax.md b/website/docs/artist_hosts_3dsmax.md index 12c1f40181..fffab8ca5d 100644 --- a/website/docs/artist_hosts_3dsmax.md +++ b/website/docs/artist_hosts_3dsmax.md @@ -30,7 +30,7 @@ By clicking the icon ```OpenPype Menu``` rolls out. Choose ```OpenPype Menu > Launcher``` to open the ```Launcher``` window. -When opened you can **choose** the **project** to work in from the list. Then choose the particular **asset** you want to work on then choose **task** +When opened you can **choose** the **project** to work in from the list. Then choose the particular **asset** you want to work on then choose **task** and finally **run 3dsmax by its icon** in the tools. ![Menu OpenPype](assets/3dsmax_tray_OP.png) @@ -65,13 +65,13 @@ If not any workfile present simply hit ```Save As``` and keep ```Subversion``` e ![Save As Dialog](assets/3dsmax_SavingFirstFile_OP.png) -OpenPype correctly names it and add version to the workfile. This basically happens whenever user trigger ```Save As``` action. Resulting into incremental version numbers like +OpenPype correctly names it and add version to the workfile. This basically happens whenever user trigger ```Save As``` action. Resulting into incremental version numbers like ```workfileName_v001``` ```workfileName_v002``` - etc. + etc. Basically meaning user is free of guessing what is the correct naming and other necessities to keep everything in order and managed. @@ -105,13 +105,13 @@ Before proceeding further please check [Glossary](artist_concepts.md) and [What ### Intro -Current OpenPype integration (ver 3.15.0) supports only ```PointCache``` and ```Camera``` families now. +Current OpenPype integration (ver 3.15.0) supports only ```PointCache```, ```Camera```, ```Geometry``` and ```Redshift Proxy``` families now. **Pointcache** family being basically any geometry outputted as Alembic cache (.abc) format **Camera** family being 3dsmax Camera object with/without animation outputted as native .max, FBX, Alembic format - +**Redshift Proxy** family being Redshift Proxy object with/without animation outputted as rs format(Redshift Proxy's very own format) --- :::note Work in progress @@ -119,7 +119,3 @@ This part of documentation is still work in progress. ::: ## ...to be added - - - - From 3c801ca1187ac3d1cf1d0da50cf27eceaae5fa30 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 18 Apr 2023 22:39:07 +0200 Subject: [PATCH 017/147] Fix imports --- openpype/hosts/maya/api/setdress.py | 8 +++----- openpype/hosts/maya/plugins/load/load_assembly.py | 15 ++++++--------- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/maya/api/setdress.py b/openpype/hosts/maya/api/setdress.py index 159bfe9eb3..0bb1f186eb 100644 --- a/openpype/hosts/maya/api/setdress.py +++ b/openpype/hosts/maya/api/setdress.py @@ -28,7 +28,9 @@ from openpype.pipeline import ( ) from openpype.hosts.maya.api.lib import ( matrix_equals, - unique_namespace + unique_namespace, + get_container_transforms, + DEFAULT_MATRIX ) log = logging.getLogger("PackageLoader") @@ -183,8 +185,6 @@ def _add(instance, representation_id, loaders, namespace, root="|"): """ - from openpype.hosts.maya.lib import get_container_transforms - # Process within the namespace with namespaced(namespace, new=False) as namespace: @@ -379,8 +379,6 @@ def update_scene(set_container, containers, current_data, new_data, new_file): """ - from openpype.hosts.maya.lib import DEFAULT_MATRIX, get_container_transforms - set_namespace = set_container['namespace'] project_name = legacy_io.active_project() diff --git a/openpype/hosts/maya/plugins/load/load_assembly.py b/openpype/hosts/maya/plugins/load/load_assembly.py index 902f38695c..275f21be5d 100644 --- a/openpype/hosts/maya/plugins/load/load_assembly.py +++ b/openpype/hosts/maya/plugins/load/load_assembly.py @@ -1,8 +1,14 @@ +import maya.cmds as cmds + from openpype.pipeline import ( load, remove_container ) +from openpype.hosts.maya.api.pipeline import containerise +from openpype.hosts.maya.api.lib import unique_namespace +from openpype.hosts.maya.api import setdress + class AssemblyLoader(load.LoaderPlugin): @@ -16,9 +22,6 @@ class AssemblyLoader(load.LoaderPlugin): def load(self, context, name, namespace, data): - from openpype.hosts.maya.api.pipeline import containerise - from openpype.hosts.maya.api.lib import unique_namespace - asset = context['asset']['name'] namespace = namespace or unique_namespace( asset + "_", @@ -26,8 +29,6 @@ class AssemblyLoader(load.LoaderPlugin): suffix="_", ) - from openpype.hosts.maya.api import setdress - containers = setdress.load_package( filepath=self.fname, name=name, @@ -50,15 +51,11 @@ class AssemblyLoader(load.LoaderPlugin): def update(self, container, representation): - from openpype import setdress return setdress.update_package(container, representation) def remove(self, container): """Remove all sub containers""" - from openpype import setdress - import maya.cmds as cmds - # Remove all members member_containers = setdress.get_contained_containers(container) for member_container in member_containers: From 7b6df29203ea2b093bc68bc4f9aec0fa6a167b8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 26 Apr 2023 12:48:51 +0200 Subject: [PATCH 018/147] Feature: Blender hook to execute python scripts at launch --- .../hooks/pre_add_run_python_script_arg.py | 61 +++++++++++++++++++ website/docs/dev_blender.md | 61 +++++++++++++++++++ website/sidebars.js | 1 + 3 files changed, 123 insertions(+) create mode 100644 openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py create mode 100644 website/docs/dev_blender.md diff --git a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py new file mode 100644 index 0000000000..7cf7b0f852 --- /dev/null +++ b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py @@ -0,0 +1,61 @@ +from pathlib import Path + +from openpype.lib import PreLaunchHook +from openpype.settings.lib import get_project_settings + + +class AddPythonScriptToLaunchArgs(PreLaunchHook): + """Add python script to be executed before Blender launch.""" + + # Append after file argument + order = 15 + app_groups = [ + "blender", + ] + + def execute(self): + # Check enabled in settings + project_name = self.data["project_name"] + project_settings = get_project_settings(project_name) + host_name = self.application.host_name + host_settings = project_settings.get(host_name) + if not host_settings: + self.log.info(f"""Host "{host_name}" doesn\'t have settings""") + return None + + # Add path to workfile to arguments + for python_script_path in self.launch_context.data.get( + "python_scripts", [] + ): + self.log.info( + f"Adding python script {python_script_path} to launch" + ) + # Test script path exists + if not Path(python_script_path).exists(): + raise ValueError( + f"Python script {python_script_path} doesn't exist." + ) + + if "--" in self.launch_context.launch_args: + # Insert before separator + separator_index = self.launch_context.launch_args.index("--") + self.launch_context.launch_args.insert( + separator_index, + "-P", + ) + self.launch_context.launch_args.insert( + separator_index + 1, + Path(python_script_path).as_posix(), + ) + else: + self.launch_context.launch_args.extend( + ["-P", Path(python_script_path).as_posix()] + ) + + # Ensure separator + if "--" not in self.launch_context.launch_args: + self.launch_context.launch_args.append("--") + + self.launch_context.launch_args.extend( + [*self.launch_context.data.get("script_args", [])] + ) diff --git a/website/docs/dev_blender.md b/website/docs/dev_blender.md new file mode 100644 index 0000000000..228447fb64 --- /dev/null +++ b/website/docs/dev_blender.md @@ -0,0 +1,61 @@ +--- +id: dev_blender +title: Blender integration +sidebar_label: Blender integration +toc_max_heading_level: 4 +--- + +## Run python script at launch +In case you need to execute a python script when Blender is started (aka [`-P`](https://docs.blender.org/manual/en/latest/advanced/command_line/arguments.html#python-options)), for example to programmatically modify a blender file for conformation, you can create an OpenPype hook as follows: + +```python +from openpype.hosts.blender.hooks.pre_add_run_python_script_arg import AddPythonScriptToLaunchArgs +from openpype.lib import PreLaunchHook + + +class MyHook(PreLaunchHook): + """Add python script to be executed before Blender launch.""" + + order = AddPythonScriptToLaunchArgs.order - 1 + app_groups = [ + "blender", + ] + + def execute(self): + self.launch_context.data.setdefault("python_scripts", []).append( + "/path/to/my_script.py" + ) +``` + +You can write a bare python script, as you could run into the [Text Editor](https://docs.blender.org/manual/en/latest/editors/text_editor.html). + +### Python script with arguments +#### Adding arguments +In case you need to pass arguments to your script, you can append them to `self.launch_context.data["script_args"]`: + +```python +self.launch_context.data.setdefault("script_args", []).append( + "--my-arg", + "value", + ) +``` + +#### Parsing arguments +You can parse arguments in your script using [argparse](https://docs.python.org/3/library/argparse.html) as follows: + +```python +import argparse + +parser = argparse.ArgumentParser( + description="Parsing arguments for my_script.py" +) +parser.add_argument( + "--my-arg", + nargs="?", + help="My argument", +) +args, unknown = arg_parser.parse_known_args( + sys.argv[sys.argv.index("--") + 1 :] +) +print(args.my_arg) +``` diff --git a/website/sidebars.js b/website/sidebars.js index 93887e00f6..c204c3fb45 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -179,6 +179,7 @@ module.exports = { ] }, "dev_deadline", + "dev_blender", "dev_colorspace" ] }; From 6de1710810b028949c86276088a988ccb83f06e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 26 Apr 2023 12:53:20 +0200 Subject: [PATCH 019/147] clean Path --- .../hosts/blender/hooks/pre_add_run_python_script_arg.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py index 7cf7b0f852..9ae96327ca 100644 --- a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py +++ b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py @@ -31,7 +31,8 @@ class AddPythonScriptToLaunchArgs(PreLaunchHook): f"Adding python script {python_script_path} to launch" ) # Test script path exists - if not Path(python_script_path).exists(): + python_script_path = Path(python_script_path) + if not python_script_path.exists(): raise ValueError( f"Python script {python_script_path} doesn't exist." ) @@ -45,11 +46,11 @@ class AddPythonScriptToLaunchArgs(PreLaunchHook): ) self.launch_context.launch_args.insert( separator_index + 1, - Path(python_script_path).as_posix(), + python_script_path.as_posix(), ) else: self.launch_context.launch_args.extend( - ["-P", Path(python_script_path).as_posix()] + ["-P", python_script_path.as_posix()] ) # Ensure separator From cdf9a10aa19b39c698a57b97abbb5858b6571de6 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 26 Apr 2023 19:10:55 +0800 Subject: [PATCH 020/147] roy's comment --- openpype/hosts/max/api/lib.py | 9 ++++++++- .../max/plugins/create/create_redshift_proxy.py | 7 ++++--- .../max/plugins/publish/extract_redshift_proxy.py | 3 +-- .../publish/validate_renderer_redshift_proxy.py | 14 +++++++++----- 4 files changed, 22 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index ad9a450cad..27d4598a3a 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -128,7 +128,14 @@ def get_all_children(parent, node_type=None): def get_current_renderer(): - """get current renderer""" + """ + Notes: + Get current renderer for Max + + Returns: + "{Current Renderer}:{Current Renderer}" + e.g. "Redshift_Renderer:Redshift_Renderer" + """ return rt.renderers.production diff --git a/openpype/hosts/max/plugins/create/create_redshift_proxy.py b/openpype/hosts/max/plugins/create/create_redshift_proxy.py index ca0891fc5b..1bddbdafae 100644 --- a/openpype/hosts/max/plugins/create/create_redshift_proxy.py +++ b/openpype/hosts/max/plugins/create/create_redshift_proxy.py @@ -18,6 +18,7 @@ class CreateRedshiftProxy(plugin.MaxCreator): instance_data, pre_create_data) # type: CreatedInstance container = rt.getNodeByName(instance.data.get("instance_node")) - - for obj in sel_obj: - obj.parent = container + if self.selected_nodes: + sel_obj = list(self.selected_nodes) + for obj in sel_obj: + obj.parent = container diff --git a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py index 0a3579d687..eb1673c4fa 100644 --- a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py +++ b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py @@ -45,7 +45,6 @@ class ExtractRedshiftProxy(publish.Extractor): representation = { 'name': 'rs', 'ext': 'rs', - # need to count the files 'files': rs_filenames if len(rs_filenames) > 1 else rs_filenames[0], # noqa "stagingDir": stagingdir, } @@ -53,7 +52,7 @@ class ExtractRedshiftProxy(publish.Extractor): self.log.info("Extracted instance '%s' to: %s" % (instance.name, stagingdir)) - # TODO: set sequence + def get_rsfiles(self, instance, startFrame, endFrame): rs_filenames = [] rs_name = instance.data["name"] diff --git a/openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py b/openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py index 3a921c386e..c834f12ae2 100644 --- a/openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py +++ b/openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py @@ -22,12 +22,13 @@ class ValidateRendererRedshiftProxy(pyblish.api.InstancePlugin): invalid = self.get_all_renderer(instance) if invalid: raise PublishValidationError("Please install Redshift for 3dsMax" - " before using this!") + " before using the Redshift proxy instance") invalid = self.get_current_renderer(instance) if invalid: - raise PublishValidationError("Current Renderer is not Redshift") + raise PublishValidationError("The Redshift proxy extraction discontinued" + "since the current renderer is not Redshift") - def get_all_renderer(self, instance): + def get_redshift_renderer(self, instance): invalid = list() max_renderers_list = str(rt.RendererClass.classes) if "Redshift_Renderer" not in max_renderers_list: @@ -46,5 +47,8 @@ class ValidateRendererRedshiftProxy(pyblish.api.InstancePlugin): @classmethod def repair(cls, instance): - if "Redshift_Renderer" in str(rt.RendererClass.classes[2]()): - rt.renderers.production = rt.RendererClass.classes[2]() + renderer_count = len(rt.RendererClass.classes) + for r in range(renderer_count): + if "Redshift_Renderer" in str(rt.RendererClass.classes[r]()): + rt.renderers.production = rt.RendererClass.classes[r]() + break From a20d37c68045d73b5f442f503dcbaf31bb8892b5 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 26 Apr 2023 19:13:33 +0800 Subject: [PATCH 021/147] hound fix --- .../hosts/max/plugins/publish/extract_redshift_proxy.py | 1 - .../max/plugins/publish/validate_renderer_redshift_proxy.py | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py index eb1673c4fa..3b44099609 100644 --- a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py +++ b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py @@ -52,7 +52,6 @@ class ExtractRedshiftProxy(publish.Extractor): self.log.info("Extracted instance '%s' to: %s" % (instance.name, stagingdir)) - def get_rsfiles(self, instance, startFrame, endFrame): rs_filenames = [] rs_name = instance.data["name"] diff --git a/openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py b/openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py index c834f12ae2..6f8a92a93c 100644 --- a/openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py +++ b/openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py @@ -22,11 +22,11 @@ class ValidateRendererRedshiftProxy(pyblish.api.InstancePlugin): invalid = self.get_all_renderer(instance) if invalid: raise PublishValidationError("Please install Redshift for 3dsMax" - " before using the Redshift proxy instance") + " before using the Redshift proxy instance") # noqa invalid = self.get_current_renderer(instance) if invalid: - raise PublishValidationError("The Redshift proxy extraction discontinued" - "since the current renderer is not Redshift") + raise PublishValidationError("The Redshift proxy extraction" + "discontinued since the current renderer is not Redshift") # noqa def get_redshift_renderer(self, instance): invalid = list() From 610a65420d521b26a3c44792368cbd4b9cec6219 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 26 Apr 2023 14:02:18 +0200 Subject: [PATCH 022/147] remove useless settings --- .../hosts/blender/hooks/pre_add_run_python_script_arg.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py index 9ae96327ca..ff3683baa9 100644 --- a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py +++ b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py @@ -14,15 +14,6 @@ class AddPythonScriptToLaunchArgs(PreLaunchHook): ] def execute(self): - # Check enabled in settings - project_name = self.data["project_name"] - project_settings = get_project_settings(project_name) - host_name = self.application.host_name - host_settings = project_settings.get(host_name) - if not host_settings: - self.log.info(f"""Host "{host_name}" doesn\'t have settings""") - return None - # Add path to workfile to arguments for python_script_path in self.launch_context.data.get( "python_scripts", [] From 4fe7ce64a2a834adcdd2763a22b8c93d532c0ef6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 26 Apr 2023 14:05:39 +0200 Subject: [PATCH 023/147] changes from comments --- .../hosts/blender/hooks/pre_add_run_python_script_arg.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py index ff3683baa9..0f959b8f54 100644 --- a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py +++ b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py @@ -1,7 +1,6 @@ from pathlib import Path from openpype.lib import PreLaunchHook -from openpype.settings.lib import get_project_settings class AddPythonScriptToLaunchArgs(PreLaunchHook): @@ -14,10 +13,11 @@ class AddPythonScriptToLaunchArgs(PreLaunchHook): ] def execute(self): + if not self.launch_context.data.get("python_scripts"): + return + # Add path to workfile to arguments - for python_script_path in self.launch_context.data.get( - "python_scripts", [] - ): + for python_script_path in self.launch_context.data["python_scripts"]: self.log.info( f"Adding python script {python_script_path} to launch" ) From edec4a2b1995adbeda6f5b681b9488f7fb3bcb08 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 26 Apr 2023 20:17:36 +0800 Subject: [PATCH 024/147] roy's comment --- .../publish/validate_renderer_redshift_proxy.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py b/openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py index 6f8a92a93c..bc82f82f3b 100644 --- a/openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py +++ b/openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py @@ -19,7 +19,7 @@ class ValidateRendererRedshiftProxy(pyblish.api.InstancePlugin): actions = [RepairAction] def process(self, instance): - invalid = self.get_all_renderer(instance) + invalid = self.get_redshift_renderer(instance) if invalid: raise PublishValidationError("Please install Redshift for 3dsMax" " before using the Redshift proxy instance") # noqa @@ -47,8 +47,8 @@ class ValidateRendererRedshiftProxy(pyblish.api.InstancePlugin): @classmethod def repair(cls, instance): - renderer_count = len(rt.RendererClass.classes) - for r in range(renderer_count): - if "Redshift_Renderer" in str(rt.RendererClass.classes[r]()): - rt.renderers.production = rt.RendererClass.classes[r]() + for Renderer in rt.RendererClass.classes: + renderer = Renderer() + if "Redshift_Renderer" in str(renderer): + rt.renderers.production = renderer break From 557cbb72cefa955f75fa6c3f1df7fe38665e1c99 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 27 Apr 2023 11:26:57 +0200 Subject: [PATCH 025/147] :recycle: escape rootless path --- openpype/modules/deadline/plugins/publish/submit_publish_job.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index f80bd40133..6ee100ddb4 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -275,7 +275,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): args = [ "--headless", 'publish', - rootless_metadata_path, + '"{}"'.format(rootless_metadata_path), "--targets", "deadline", "--targets", "farm" ] From 84cef9d3cf6135fd26b490f57e9bc68d9da36ace Mon Sep 17 00:00:00 2001 From: Michael reda Date: Tue, 2 May 2023 11:12:18 +0200 Subject: [PATCH 026/147] handel long lines --- openpype/modules/kitsu/kitsu_module.py | 19 ++++++-- .../modules/kitsu/utils/update_op_with_zou.py | 48 +++++++++---------- 2 files changed, 40 insertions(+), 27 deletions(-) diff --git a/openpype/modules/kitsu/kitsu_module.py b/openpype/modules/kitsu/kitsu_module.py index f4e3dd5691..7c9d888aa7 100644 --- a/openpype/modules/kitsu/kitsu_module.py +++ b/openpype/modules/kitsu/kitsu_module.py @@ -124,10 +124,23 @@ def push_to_zou(login, password): @cli_main.command() -@click.option("-prjs", "--projects", envvar="SYNC_PROJECTS", help="Sync specific kitsu projects") -@click.option("-l", "--login", envvar="KITSU_LOGIN", help="Kitsu login") @click.option( - "-p", "--password", envvar="KITSU_PWD", help="Password for kitsu username" + "-prjs", + "--projects", + envvar="SYNC_PROJECTS", + help="Sync specific kitsu projects" +) +@click.option( + "-l", + "--login", + envvar="KITSU_LOGIN", + help="Kitsu login" +) +@click.option( + "-p", + "--password", + envvar="KITSU_PWD", + help="Password for kitsu username" ) def sync_service(login, password, projects="^"): """Synchronize openpype database from Zou sever database. diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index a397198a13..ad8ccd9f3f 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -63,11 +63,11 @@ def set_op_project(dbcon: AvalonMongoDB, project_id: str): def update_op_assets( - dbcon: AvalonMongoDB, - gazu_project: dict, - project_doc: dict, - entities_list: List[dict], - asset_doc_ids: Dict[str, dict], + dbcon: AvalonMongoDB, + gazu_project: dict, + project_doc: dict, + entities_list: List[dict], + asset_doc_ids: Dict[str, dict], ) -> List[Dict[str, dict]]: """Update OpenPype assets. Set 'data' and 'parent' fields. @@ -210,10 +210,10 @@ def update_op_assets( item.get("entity_type_id") if item_type == "Asset" else None - # Else, fallback on usual hierarchy - or item.get("parent_id") - or item.get("episode_id") - or item.get("source_id") + # Else, fallback on usual hierarchy + or item.get("parent_id") + or item.get("episode_id") + or item.get("source_id") ) # Substitute item type for general classification (assets or shots) @@ -350,7 +350,7 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: "config.tasks": { t["name"]: {"short_name": t.get("short_name", t["name"])} for t in gazu.task.all_task_types_for_project(project) - or gazu.task.all_task_types() + or gazu.task.all_task_types() }, "data": project_data, } @@ -359,7 +359,8 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: def sync_all_projects( - login: str, password: str, ignore_projects: list = None, specific_projects: list = None + login: str, password: str, ignore_projects: list = None, + specific_projects: list = None ): """Update all OP projects in DB with Zou data. @@ -383,7 +384,6 @@ def sync_all_projects( dbcon.install() all_projects = gazu.project.all_projects() - project_to_sync = [] if specific_projects == ['*']: project_to_sync = all_projects @@ -435,8 +435,8 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): # Do not sync closed kitsu project that is not found in openpype if ( - project['project_status_name'] == "Closed" - and not get_project(project['name']) + project['project_status_name'] == "Closed" + and not get_project(project['name']) ): return @@ -451,10 +451,10 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): all_entities = [ item for item in all_assets - + all_asset_types - + all_episodes - + all_seqs - + all_shots + + all_asset_types + + all_episodes + + all_seqs + + all_shots if naming_pattern.match(item["name"]) ] @@ -526,12 +526,12 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): [ UpdateOne({"_id": id}, update) for id, update in update_op_assets( - dbcon, - project, - project_dict, - all_entities, - zou_ids_and_asset_docs, - ) + dbcon, + project, + project_dict, + all_entities, + zou_ids_and_asset_docs, + ) ] ) From 2a9a7d6fd4f0a96dc70b1f61525d5b8f1ac05eb5 Mon Sep 17 00:00:00 2001 From: Michael <71185790+Michaelredaa@users.noreply.github.com> Date: Wed, 3 May 2023 12:18:43 +0300 Subject: [PATCH 027/147] handle reviews --- openpype/modules/kitsu/kitsu_module.py | 19 ++---- .../modules/kitsu/utils/update_op_with_zou.py | 62 +++++++++---------- 2 files changed, 36 insertions(+), 45 deletions(-) diff --git a/openpype/modules/kitsu/kitsu_module.py b/openpype/modules/kitsu/kitsu_module.py index 7c9d888aa7..2a3a0f3bff 100644 --- a/openpype/modules/kitsu/kitsu_module.py +++ b/openpype/modules/kitsu/kitsu_module.py @@ -125,9 +125,10 @@ def push_to_zou(login, password): @cli_main.command() @click.option( - "-prjs", - "--projects", - envvar="SYNC_PROJECTS", + "-prj", + "--project", + multiple=True, + default=[""] help="Sync specific kitsu projects" ) @click.option( @@ -142,26 +143,18 @@ def push_to_zou(login, password): envvar="KITSU_PWD", help="Password for kitsu username" ) -def sync_service(login, password, projects="^"): +def sync_service(login, password, project): """Synchronize openpype database from Zou sever database. Args: login (str): Kitsu user login password (str): Kitsu user password projects (str): specific kitsu projects - - SYNC_PROJECTS: - *: all projects - ^: dont sync any project just listen - "project01 project02 ...": to choose custom projects - - """ from .utils.update_op_with_zou import sync_all_projects from .utils.sync_service import start_listeners - projects = projects.strip() - projects = projects.split(' ') + projects = ' '.join(project) sync_all_projects(login, password, specific_projects=projects) start_listeners(login, password) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index ad8ccd9f3f..7983765e83 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -63,11 +63,11 @@ def set_op_project(dbcon: AvalonMongoDB, project_id: str): def update_op_assets( - dbcon: AvalonMongoDB, - gazu_project: dict, - project_doc: dict, - entities_list: List[dict], - asset_doc_ids: Dict[str, dict], + dbcon: AvalonMongoDB, + gazu_project: dict, + project_doc: dict, + entities_list: List[dict], + asset_doc_ids: Dict[str, dict], ) -> List[Dict[str, dict]]: """Update OpenPype assets. Set 'data' and 'parent' fields. @@ -210,10 +210,10 @@ def update_op_assets( item.get("entity_type_id") if item_type == "Asset" else None - # Else, fallback on usual hierarchy - or item.get("parent_id") - or item.get("episode_id") - or item.get("source_id") + # Else, fallback on usual hierarchy + or item.get("parent_id") + or item.get("episode_id") + or item.get("source_id") ) # Substitute item type for general classification (assets or shots) @@ -350,7 +350,7 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: "config.tasks": { t["name"]: {"short_name": t.get("short_name", t["name"])} for t in gazu.task.all_task_types_for_project(project) - or gazu.task.all_task_types() + or gazu.task.all_task_types() }, "data": project_data, } @@ -358,9 +358,8 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: ) -def sync_all_projects( - login: str, password: str, ignore_projects: list = None, - specific_projects: list = None +def sync_all_projects(login: str, password: str, ignore_projects: list = None, + filter_projects: list = None ): """Update all OP projects in DB with Zou data. @@ -368,7 +367,7 @@ def sync_all_projects( login (str): Kitsu user login password (str): Kitsu user password ignore_projects (list): List of unsynced project names - specific_projects (list): List of synced project names + filter_projects (list): List of filter project names to sync with Raises: gazu.exception.AuthFailedException: Wrong user login and/or password """ @@ -385,22 +384,21 @@ def sync_all_projects( all_projects = gazu.project.all_projects() project_to_sync = [] - if specific_projects == ['*']: + + if not filter_projects: + # listen only + return + + if filter_projects == ['*']: project_to_sync = all_projects - elif specific_projects == ['^']: - return - - elif isinstance(specific_projects, list): - all_kitsu_projects = {p['name']: p for p in all_projects} - for proj_name in specific_projects: - if proj_name in all_kitsu_projects: - project_to_sync.append(all_kitsu_projects[proj_name]) - else: - log.info(f'`{proj_name}` project does not exists in kitsu.' - f' Please make sure you write the project correctly.') - else: - return + all_kitsu_projects = {p['name']: p for p in all_projects} + for proj_name in filter_projects: + if proj_name in all_kitsu_projects: + project_to_sync.append(all_kitsu_projects[proj_name]) + else: + log.info(f'`{proj_name}` project does not exist in Kitsu.' + f' Please make sure the project is spelled correctly.') for project in project_to_sync: if ignore_projects and project["name"] in ignore_projects: @@ -451,10 +449,10 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): all_entities = [ item for item in all_assets - + all_asset_types - + all_episodes - + all_seqs - + all_shots + + all_asset_types + + all_episodes + + all_seqs + + all_shots if naming_pattern.match(item["name"]) ] From 0a3d206680ed7b21cfb30f1800ae1625e284793d Mon Sep 17 00:00:00 2001 From: Michael <71185790+Michaelredaa@users.noreply.github.com> Date: Wed, 3 May 2023 12:23:17 +0300 Subject: [PATCH 028/147] solve syntex error --- openpype/modules/kitsu/kitsu_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/kitsu/kitsu_module.py b/openpype/modules/kitsu/kitsu_module.py index 2a3a0f3bff..59ad2efd29 100644 --- a/openpype/modules/kitsu/kitsu_module.py +++ b/openpype/modules/kitsu/kitsu_module.py @@ -128,7 +128,7 @@ def push_to_zou(login, password): "-prj", "--project", multiple=True, - default=[""] + default=[""], help="Sync specific kitsu projects" ) @click.option( From 6e9b6f6bef0c290dd46b7b3669fad2d3b338c0e8 Mon Sep 17 00:00:00 2001 From: Michael <71185790+Michaelredaa@users.noreply.github.com> Date: Wed, 3 May 2023 12:38:42 +0300 Subject: [PATCH 029/147] correct arg name in sync_all_projects --- openpype/modules/kitsu/kitsu_module.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/modules/kitsu/kitsu_module.py b/openpype/modules/kitsu/kitsu_module.py index 59ad2efd29..bd8ade62d8 100644 --- a/openpype/modules/kitsu/kitsu_module.py +++ b/openpype/modules/kitsu/kitsu_module.py @@ -149,12 +149,10 @@ def sync_service(login, password, project): Args: login (str): Kitsu user login password (str): Kitsu user password - projects (str): specific kitsu projects + project (str): specific kitsu projects """ from .utils.update_op_with_zou import sync_all_projects from .utils.sync_service import start_listeners - projects = ' '.join(project) - - sync_all_projects(login, password, specific_projects=projects) + sync_all_projects(login, password, filter_projects=project) start_listeners(login, password) From ad4f5876ce18239b244d59332ed40b5469cc0433 Mon Sep 17 00:00:00 2001 From: Michael <71185790+Michaelredaa@users.noreply.github.com> Date: Wed, 3 May 2023 12:40:32 +0300 Subject: [PATCH 030/147] update condition to sync specific projects --- .../modules/kitsu/utils/update_op_with_zou.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index 7983765e83..40e4191508 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -389,16 +389,18 @@ def sync_all_projects(login: str, password: str, ignore_projects: list = None, # listen only return - if filter_projects == ['*']: + if '*' in filter_projects: + # all projects project_to_sync = all_projects - all_kitsu_projects = {p['name']: p for p in all_projects} - for proj_name in filter_projects: - if proj_name in all_kitsu_projects: - project_to_sync.append(all_kitsu_projects[proj_name]) - else: - log.info(f'`{proj_name}` project does not exist in Kitsu.' - f' Please make sure the project is spelled correctly.') + else: + all_kitsu_projects = {p['name']: p for p in all_projects} + for proj_name in filter_projects: + if proj_name in all_kitsu_projects: + project_to_sync.append(all_kitsu_projects[proj_name]) + else: + log.info(f'`{proj_name}` project does not exist in Kitsu.' + f' Please make sure the project is spelled correctly.') for project in project_to_sync: if ignore_projects and project["name"] in ignore_projects: From d6e7cd638d403f0d3f8ad53e7fef444122b1dc39 Mon Sep 17 00:00:00 2001 From: Michael <71185790+Michaelredaa@users.noreply.github.com> Date: Wed, 3 May 2023 13:02:08 +0300 Subject: [PATCH 031/147] update kitsu docs --- website/docs/module_kitsu.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/website/docs/module_kitsu.md b/website/docs/module_kitsu.md index d79c78fecf..4b827b3802 100644 --- a/website/docs/module_kitsu.md +++ b/website/docs/module_kitsu.md @@ -16,10 +16,18 @@ If you want to connect Kitsu to OpenPype you have to set the `Server` url in Kit This setting is available for all the users of the OpenPype instance. ## Synchronize -Updating OP with Kitsu data is executed running the `sync-service`, which requires to provide your Kitsu credentials with `-l, --login` and `-p, --password` or by setting the environment variables `KITSU_LOGIN` and `KITSU_PWD`. This process will request data from Kitsu and create/delete/update OP assets. +Updating OP with Kitsu data is executed running the `sync-service`, which requires to provide your Kitsu credentials with `-l, --login` and `-p, --password` or by setting the environment variables `KITSU_LOGIN` and `KITSU_PWD`. This process will request data from Kitsu projects with `-proj, --project` and create/delete/update OP assets. Once this sync is done, the thread will automatically start a loop to listen to Kitsu events. +The args for `-proj, --project` accept multiple project name, `-proj *` to sync all active projects, and the default value to start a loop to listen to Kitsu events only without any sync. ```bash +// sync specific projects then run listen +openpype_console module kitsu sync-service -l me@domain.ext -p my_password -proj project_name01 -proj project_name02 + +// sync all projects then run listen +openpype_console module kitsu sync-service -l me@domain.ext -p my_password -proj * + +// start listen only openpype_console module kitsu sync-service -l me@domain.ext -p my_password ``` From 925ea3fe9312a3e78df1604e370a019cb2adbe5c Mon Sep 17 00:00:00 2001 From: Michael <71185790+Michaelredaa@users.noreply.github.com> Date: Wed, 3 May 2023 20:14:33 +0300 Subject: [PATCH 032/147] Update module_kitsu docs --- website/docs/module_kitsu.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/website/docs/module_kitsu.md b/website/docs/module_kitsu.md index 4b827b3802..970bfb275e 100644 --- a/website/docs/module_kitsu.md +++ b/website/docs/module_kitsu.md @@ -16,19 +16,22 @@ If you want to connect Kitsu to OpenPype you have to set the `Server` url in Kit This setting is available for all the users of the OpenPype instance. ## Synchronize -Updating OP with Kitsu data is executed running the `sync-service`, which requires to provide your Kitsu credentials with `-l, --login` and `-p, --password` or by setting the environment variables `KITSU_LOGIN` and `KITSU_PWD`. This process will request data from Kitsu projects with `-proj, --project` and create/delete/update OP assets. +Updating OP with Kitsu data is executed running the `sync-service`, which requires to provide your Kitsu credentials with `-l, --login` and `-p, --password` or by setting the environment variables `KITSU_LOGIN` and `KITSU_PWD`. This process will request data from Kitsu and create/delete/update OP assets. Once this sync is done, the thread will automatically start a loop to listen to Kitsu events. -The args for `-proj, --project` accept multiple project name, `-proj *` to sync all active projects, and the default value to start a loop to listen to Kitsu events only without any sync. +- `-prj, --project` This flag accepts multiple project name to sync specific projects, and the default to sync all projects. +- `-lo, --listen-only` This flag to run listen to Kitsu events only without any sync. + +Note: You must use one argument of `-pro` or `-lo`, because the listen only flag override syncing flag. ```bash -// sync specific projects then run listen -openpype_console module kitsu sync-service -l me@domain.ext -p my_password -proj project_name01 -proj project_name02 - // sync all projects then run listen -openpype_console module kitsu sync-service -l me@domain.ext -p my_password -proj * +openpype_console module kitsu sync-service -l me@domain.ext -p my_password + +// sync specific projects then run listen +openpype_console module kitsu sync-service -l me@domain.ext -p my_password -prj project_name01 -prj project_name02 // start listen only -openpype_console module kitsu sync-service -l me@domain.ext -p my_password +openpype_console module kitsu sync-service -l me@domain.ext -p my_password -lo ``` ### Events listening From c5dd4d21eed2b7d9b67ecdb980a291742c050e7c Mon Sep 17 00:00:00 2001 From: Michael <71185790+Michaelredaa@users.noreply.github.com> Date: Wed, 3 May 2023 20:16:23 +0300 Subject: [PATCH 033/147] Add listen-only flag to sync --- openpype/modules/kitsu/kitsu_module.py | 27 +++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/openpype/modules/kitsu/kitsu_module.py b/openpype/modules/kitsu/kitsu_module.py index bd8ade62d8..319b5de16b 100644 --- a/openpype/modules/kitsu/kitsu_module.py +++ b/openpype/modules/kitsu/kitsu_module.py @@ -124,13 +124,6 @@ def push_to_zou(login, password): @cli_main.command() -@click.option( - "-prj", - "--project", - multiple=True, - default=[""], - help="Sync specific kitsu projects" -) @click.option( "-l", "--login", @@ -143,16 +136,32 @@ def push_to_zou(login, password): envvar="KITSU_PWD", help="Password for kitsu username" ) -def sync_service(login, password, project): +@click.option( + "-prj", + "--project", + multiple=True, + default=[], + help="Sync specific kitsu projects" +) +@click.option( + "-lo", + "--listen_only/--listen-only", + default=False, + help="Listen to events only without any syncing" +) +def sync_service(login, password, project, listen_only): """Synchronize openpype database from Zou sever database. Args: login (str): Kitsu user login password (str): Kitsu user password project (str): specific kitsu projects + listen_only (bool): run listen only without any syncing """ from .utils.update_op_with_zou import sync_all_projects from .utils.sync_service import start_listeners - sync_all_projects(login, password, filter_projects=project) + if not listen_only: + sync_all_projects(login, password, filter_projects=project) + start_listeners(login, password) From 44e533ff3306aa218a25960a1a41062e3fa82b5a Mon Sep 17 00:00:00 2001 From: Michael <71185790+Michaelredaa@users.noreply.github.com> Date: Wed, 3 May 2023 20:20:43 +0300 Subject: [PATCH 034/147] Update sync_all_projects function with filtered projects --- .../modules/kitsu/utils/update_op_with_zou.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index 40e4191508..bfb4bd58fa 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -367,7 +367,7 @@ def sync_all_projects(login: str, password: str, ignore_projects: list = None, login (str): Kitsu user login password (str): Kitsu user password ignore_projects (list): List of unsynced project names - filter_projects (list): List of filter project names to sync with + filter_projects (tuple): Tuple of filter project names to sync with Raises: gazu.exception.AuthFailedException: Wrong user login and/or password """ @@ -385,15 +385,7 @@ def sync_all_projects(login: str, password: str, ignore_projects: list = None, project_to_sync = [] - if not filter_projects: - # listen only - return - - if '*' in filter_projects: - # all projects - project_to_sync = all_projects - - else: + if filter_projects: all_kitsu_projects = {p['name']: p for p in all_projects} for proj_name in filter_projects: if proj_name in all_kitsu_projects: @@ -401,6 +393,9 @@ def sync_all_projects(login: str, password: str, ignore_projects: list = None, else: log.info(f'`{proj_name}` project does not exist in Kitsu.' f' Please make sure the project is spelled correctly.') + else: + # all project + project_to_sync = all_projects for project in project_to_sync: if ignore_projects and project["name"] in ignore_projects: From 8242e61ad893843f4539e6a1517e50aa4e785de2 Mon Sep 17 00:00:00 2001 From: Michael reda Date: Fri, 5 May 2023 13:54:24 +0200 Subject: [PATCH 035/147] update linting --- openpype/modules/kitsu/kitsu_module.py | 18 ++----- .../modules/kitsu/utils/update_op_with_zou.py | 54 ++++++++++--------- 2 files changed, 33 insertions(+), 39 deletions(-) diff --git a/openpype/modules/kitsu/kitsu_module.py b/openpype/modules/kitsu/kitsu_module.py index 319b5de16b..dec19989ea 100644 --- a/openpype/modules/kitsu/kitsu_module.py +++ b/openpype/modules/kitsu/kitsu_module.py @@ -94,7 +94,7 @@ class KitsuModule(OpenPypeModule, IPluginPaths, ITrayAction): return { "publish": [os.path.join(current_dir, "plugins", "publish")], - "actions": [os.path.join(current_dir, "actions")] + "actions": [os.path.join(current_dir, "actions")], } def cli(self, click_group): @@ -124,30 +124,22 @@ def push_to_zou(login, password): @cli_main.command() +@click.option("-l", "--login", envvar="KITSU_LOGIN", help="Kitsu login") @click.option( - "-l", - "--login", - envvar="KITSU_LOGIN", - help="Kitsu login" -) -@click.option( - "-p", - "--password", - envvar="KITSU_PWD", - help="Password for kitsu username" + "-p", "--password", envvar="KITSU_PWD", help="Password for kitsu username" ) @click.option( "-prj", "--project", multiple=True, default=[], - help="Sync specific kitsu projects" + help="Sync specific kitsu projects", ) @click.option( "-lo", "--listen_only/--listen-only", default=False, - help="Listen to events only without any syncing" + help="Listen to events only without any syncing", ) def sync_service(login, password, project, listen_only): """Synchronize openpype database from Zou sever database. diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index bfb4bd58fa..4ad08bb739 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -94,9 +94,7 @@ def update_op_assets( if not item_doc: # Create asset op_asset = create_op_asset(item) insert_result = dbcon.insert_one(op_asset) - item_doc = get_asset_by_id( - project_name, insert_result.inserted_id - ) + item_doc = get_asset_by_id(project_name, insert_result.inserted_id) # Update asset item_data = deepcopy(item_doc["data"]) @@ -210,10 +208,10 @@ def update_op_assets( item.get("entity_type_id") if item_type == "Asset" else None - # Else, fallback on usual hierarchy - or item.get("parent_id") - or item.get("episode_id") - or item.get("source_id") + # Else, fallback on usual hierarchy + or item.get("parent_id") + or item.get("episode_id") + or item.get("source_id") ) # Substitute item type for general classification (assets or shots) @@ -329,7 +327,7 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: "code": project_code, "fps": float(project["fps"]), "zou_id": project["id"], - "active": project['project_status_name'] != "Closed", + "active": project["project_status_name"] != "Closed", } ) @@ -350,7 +348,7 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: "config.tasks": { t["name"]: {"short_name": t.get("short_name", t["name"])} for t in gazu.task.all_task_types_for_project(project) - or gazu.task.all_task_types() + or gazu.task.all_task_types() }, "data": project_data, } @@ -358,8 +356,11 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: ) -def sync_all_projects(login: str, password: str, ignore_projects: list = None, - filter_projects: list = None +def sync_all_projects( + login: str, + password: str, + ignore_projects: list = None, + filter_projects: list = None, ): """Update all OP projects in DB with Zou data. @@ -386,13 +387,15 @@ def sync_all_projects(login: str, password: str, ignore_projects: list = None, project_to_sync = [] if filter_projects: - all_kitsu_projects = {p['name']: p for p in all_projects} + all_kitsu_projects = {p["name"]: p for p in all_projects} for proj_name in filter_projects: if proj_name in all_kitsu_projects: project_to_sync.append(all_kitsu_projects[proj_name]) else: - log.info(f'`{proj_name}` project does not exist in Kitsu.' - f' Please make sure the project is spelled correctly.') + log.info( + f"`{proj_name}` project does not exist in Kitsu." + f" Please make sure the project is spelled correctly." + ) else: # all project project_to_sync = all_projects @@ -424,14 +427,13 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): # Get all statuses for projects from Kitsu all_status = gazu.project.all_project_status() for status in all_status: - if project['project_status_id'] == status['id']: - project['project_status_name'] = status['name'] + if project["project_status_id"] == status["id"]: + project["project_status_name"] = status["name"] break # Do not sync closed kitsu project that is not found in openpype - if ( - project['project_status_name'] == "Closed" - and not get_project(project['name']) + if project["project_status_name"] == "Closed" and not get_project( + project["name"] ): return @@ -460,7 +462,7 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): log.info("Project created: {}".format(project_name)) bulk_writes.append(write_project_to_op(project, dbcon)) - if project['project_status_name'] == "Closed": + if project["project_status_name"] == "Closed": return # Try to find project document @@ -521,12 +523,12 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): [ UpdateOne({"_id": id}, update) for id, update in update_op_assets( - dbcon, - project, - project_dict, - all_entities, - zou_ids_and_asset_docs, - ) + dbcon, + project, + project_dict, + all_entities, + zou_ids_and_asset_docs, + ) ] ) From 07cf84e6481d013e79493e3d87c0189ab9ecf2cd Mon Sep 17 00:00:00 2001 From: Michael reda Date: Fri, 5 May 2023 14:14:12 +0200 Subject: [PATCH 036/147] add variable name to `@click.option` --- openpype/modules/kitsu/kitsu_module.py | 11 +++++++---- openpype/modules/kitsu/utils/update_op_with_zou.py | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/openpype/modules/kitsu/kitsu_module.py b/openpype/modules/kitsu/kitsu_module.py index dec19989ea..8d2d5ccd60 100644 --- a/openpype/modules/kitsu/kitsu_module.py +++ b/openpype/modules/kitsu/kitsu_module.py @@ -131,29 +131,32 @@ def push_to_zou(login, password): @click.option( "-prj", "--project", + "projects", multiple=True, default=[], help="Sync specific kitsu projects", ) @click.option( "-lo", - "--listen_only/--listen-only", + "--listen-only", + "listen_only", + is_flag=True, default=False, help="Listen to events only without any syncing", ) -def sync_service(login, password, project, listen_only): +def sync_service(login, password, projects, listen_only): """Synchronize openpype database from Zou sever database. Args: login (str): Kitsu user login password (str): Kitsu user password - project (str): specific kitsu projects + projects (tuple): specific kitsu projects listen_only (bool): run listen only without any syncing """ from .utils.update_op_with_zou import sync_all_projects from .utils.sync_service import start_listeners if not listen_only: - sync_all_projects(login, password, filter_projects=project) + sync_all_projects(login, password, filter_projects=projects) start_listeners(login, password) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index 4ad08bb739..b495cd1bea 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -360,7 +360,7 @@ def sync_all_projects( login: str, password: str, ignore_projects: list = None, - filter_projects: list = None, + filter_projects: tuple = None, ): """Update all OP projects in DB with Zou data. From a5efddf96926cd2a3137dc9a5ef6e7fd272d3326 Mon Sep 17 00:00:00 2001 From: Michael <71185790+Michaelredaa@users.noreply.github.com> Date: Fri, 5 May 2023 15:47:39 +0300 Subject: [PATCH 037/147] Update docs to be more specific MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: FΓ©lix David --- website/docs/module_kitsu.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/module_kitsu.md b/website/docs/module_kitsu.md index 970bfb275e..9695542723 100644 --- a/website/docs/module_kitsu.md +++ b/website/docs/module_kitsu.md @@ -30,7 +30,7 @@ openpype_console module kitsu sync-service -l me@domain.ext -p my_password // sync specific projects then run listen openpype_console module kitsu sync-service -l me@domain.ext -p my_password -prj project_name01 -prj project_name02 -// start listen only +// start listen only for all projects openpype_console module kitsu sync-service -l me@domain.ext -p my_password -lo ``` From 86fcab6f8d423fd6d719f5ba50f63e72e278e056 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 8 May 2023 15:20:25 +0800 Subject: [PATCH 038/147] refractor the creator for custom modifiers --- .../hosts/max/plugins/create/create_redshift_proxy.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/max/plugins/create/create_redshift_proxy.py b/openpype/hosts/max/plugins/create/create_redshift_proxy.py index 1bddbdafae..8c71feb40f 100644 --- a/openpype/hosts/max/plugins/create/create_redshift_proxy.py +++ b/openpype/hosts/max/plugins/create/create_redshift_proxy.py @@ -12,13 +12,8 @@ class CreateRedshiftProxy(plugin.MaxCreator): def create(self, subset_name, instance_data, pre_create_data): from pymxs import runtime as rt - sel_obj = list(rt.selection) - instance = super(CreateRedshiftProxy, self).create( + + _ = super(CreateRedshiftProxy, self).create( subset_name, instance_data, pre_create_data) # type: CreatedInstance - container = rt.getNodeByName(instance.data.get("instance_node")) - if self.selected_nodes: - sel_obj = list(self.selected_nodes) - for obj in sel_obj: - obj.parent = container From e45098c2841c2390c42d7b46f2d1688a5220fa0a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 8 May 2023 15:21:27 +0800 Subject: [PATCH 039/147] hound fix --- openpype/hosts/max/plugins/create/create_redshift_proxy.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/max/plugins/create/create_redshift_proxy.py b/openpype/hosts/max/plugins/create/create_redshift_proxy.py index 8c71feb40f..698ea82b69 100644 --- a/openpype/hosts/max/plugins/create/create_redshift_proxy.py +++ b/openpype/hosts/max/plugins/create/create_redshift_proxy.py @@ -11,7 +11,6 @@ class CreateRedshiftProxy(plugin.MaxCreator): icon = "gear" def create(self, subset_name, instance_data, pre_create_data): - from pymxs import runtime as rt _ = super(CreateRedshiftProxy, self).create( subset_name, From 6252e4b6c5c57e2a0f9f60fd0ad53cdcb7d26c77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 10 May 2023 10:07:12 +0200 Subject: [PATCH 040/147] skipping if python script doesn't exist --- openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py index 0f959b8f54..8015a15de8 100644 --- a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py +++ b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py @@ -24,8 +24,8 @@ class AddPythonScriptToLaunchArgs(PreLaunchHook): # Test script path exists python_script_path = Path(python_script_path) if not python_script_path.exists(): - raise ValueError( - f"Python script {python_script_path} doesn't exist." + raise self.log.warning( + f"Python script {python_script_path} doesn't exist. Skipped..." ) if "--" in self.launch_context.launch_args: From 52ad442a9cacdd7675bb49c9fbcd144a2400df51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 10 May 2023 10:10:12 +0200 Subject: [PATCH 041/147] lint --- openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py index 8015a15de8..2d1b773c5f 100644 --- a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py +++ b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py @@ -25,7 +25,8 @@ class AddPythonScriptToLaunchArgs(PreLaunchHook): python_script_path = Path(python_script_path) if not python_script_path.exists(): raise self.log.warning( - f"Python script {python_script_path} doesn't exist. Skipped..." + f"Python script {python_script_path} doesn't exist. " + "Skipped..." ) if "--" in self.launch_context.launch_args: From 9a6ae240e2fbafe693701cb791656e44e3440d74 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 11 May 2023 17:00:29 +0800 Subject: [PATCH 042/147] using currentfile for redshift renderer --- .../hosts/max/plugins/publish/collect_render.py | 6 ++++-- .../plugins/publish/submit_max_deadline.py | 16 +++++++++++----- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index b040467522..0d4dbc4521 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -5,7 +5,7 @@ import pyblish.api from pymxs import runtime as rt from openpype.pipeline import get_current_asset_name -from openpype.hosts.max.api.lib import get_max_version +from openpype.hosts.max.api.lib import get_max_version, get_current_renderer from openpype.hosts.max.api.lib_renderproducts import RenderProducts from openpype.client import get_last_version_by_subset_name @@ -38,7 +38,8 @@ class CollectRender(pyblish.api.InstancePlugin): version_doc = get_last_version_by_subset_name(project_name, instance.name, asset_id) - + renderer_class = get_current_renderer() + renderer = str(renderer_class).split(":")[0] self.log.debug("version_doc: {0}".format(version_doc)) version_int = 1 if version_doc: @@ -59,6 +60,7 @@ class CollectRender(pyblish.api.InstancePlugin): "source": filepath, "expectedFiles": render_layer_files, "plugin": "3dsmax", + "renderer": renderer, "frameStart": context.data['frameStart'], "frameEnd": context.data['frameEnd'], "version": version_int, diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index c728b6b9c7..0cf4990428 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -14,7 +14,6 @@ from openpype.pipeline import ( ) from openpype.settings import get_project_settings from openpype.hosts.max.api.lib import ( - get_current_renderer, get_multipass_setting ) from openpype.hosts.max.api.lib_rendersettings import RenderSettings @@ -157,6 +156,12 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, return plugin_payload + def from_published_scene(self, replace_in_path=True): + instance = self._instance + if instance.data["renderer"]== "Redshift_renderer": + file_path = self.scene_path + return file_path + def process_submission(self): instance = self._instance @@ -185,6 +190,8 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, instance = self._instance job_info = copy.deepcopy(self.job_info) plugin_info = copy.deepcopy(self.plugin_info) + if instance.data["renderer"] == "Redshift_Renderer": + self.log.debug("Using Redshift...published scene wont be used..") plugin_data = {} project_setting = get_project_settings( legacy_io.Session["AVALON_PROJECT"] @@ -202,7 +209,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, old_output_dir = os.path.dirname(expected_files[0]) output_beauty = RenderSettings().get_render_output(instance.name, old_output_dir) - filepath = self.from_published_scene() + filepath = self.scene_path def _clean_name(path): return os.path.splitext(os.path.basename(path))[0] @@ -214,9 +221,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, output_beauty = output_beauty.replace("\\", "/") plugin_data["RenderOutput"] = output_beauty - renderer_class = get_current_renderer() - renderer = str(renderer_class).split(":")[0] - if renderer in [ + if instance.data["renderer"] in [ "ART_Renderer", "Redshift_Renderer", "V_Ray_6_Hotfix_3", @@ -227,6 +232,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, render_elem_list = RenderSettings().get_render_element() for i, element in enumerate(render_elem_list): element = element.replace(orig_scene, new_scene) + element = element.replace("\\", "/") plugin_data["RenderElementOutputFilename%d" % i] = element # noqa self.log.debug("plugin data:{}".format(plugin_data)) From be386a36880a7868a4e46ba42fd49bfa08df6905 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 11 May 2023 17:05:56 +0800 Subject: [PATCH 043/147] hound fix --- .../modules/deadline/plugins/publish/submit_max_deadline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index 0cf4990428..a66d4d630a 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -158,7 +158,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, def from_published_scene(self, replace_in_path=True): instance = self._instance - if instance.data["renderer"]== "Redshift_renderer": + if instance.data["renderer"] == "Redshift_renderer": file_path = self.scene_path return file_path From 2369d50224cd4768c182cee7004df4da7d381f27 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 15 May 2023 11:25:56 +0100 Subject: [PATCH 044/147] Skipping rendersetup for members. --- .../maya/plugins/publish/validate_instance_has_members.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/validate_instance_has_members.py b/openpype/hosts/maya/plugins/publish/validate_instance_has_members.py index 4870f27bff..fcafc2be79 100644 --- a/openpype/hosts/maya/plugins/publish/validate_instance_has_members.py +++ b/openpype/hosts/maya/plugins/publish/validate_instance_has_members.py @@ -14,6 +14,11 @@ class ValidateInstanceHasMembers(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance): + # Allow renderlayer and workfile to be empty + skip_families = ["workfile", "renderlayer", "rendersetup"] + if instance.data.get("family") in skip_families: + return + invalid = list() if not instance.data["setMembers"]: objectset_name = instance.data['name'] From b47d172944aa1e0039b3fb554093dbc7508b3033 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 15 May 2023 12:30:32 +0100 Subject: [PATCH 045/147] Move family check to process --- .../plugins/publish/validate_instance_has_members.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_instance_has_members.py b/openpype/hosts/maya/plugins/publish/validate_instance_has_members.py index fcafc2be79..63849cfd12 100644 --- a/openpype/hosts/maya/plugins/publish/validate_instance_has_members.py +++ b/openpype/hosts/maya/plugins/publish/validate_instance_has_members.py @@ -13,12 +13,6 @@ class ValidateInstanceHasMembers(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance): - - # Allow renderlayer and workfile to be empty - skip_families = ["workfile", "renderlayer", "rendersetup"] - if instance.data.get("family") in skip_families: - return - invalid = list() if not instance.data["setMembers"]: objectset_name = instance.data['name'] @@ -27,6 +21,10 @@ class ValidateInstanceHasMembers(pyblish.api.InstancePlugin): return invalid def process(self, instance): + # Allow renderlayer and workfile to be empty + skip_families = ["workfile", "renderlayer", "rendersetup"] + if instance.data.get("family") in skip_families: + return invalid = self.get_invalid(instance) if invalid: From 11d47eb9407be381a6a10731b683886f9f26f0da Mon Sep 17 00:00:00 2001 From: Petr Dvorak Date: Tue, 16 May 2023 10:28:24 +0200 Subject: [PATCH 046/147] Company name and URL changed --- inno_setup.iss | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/inno_setup.iss b/inno_setup.iss index 3adde52a8b..418bedbd4d 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -14,10 +14,10 @@ AppId={{B9E9DF6A-5BDA-42DD-9F35-C09D564C4D93} AppName={#MyAppName} AppVersion={#AppVer} AppVerName={#MyAppName} version {#AppVer} -AppPublisher=Orbi Tools s.r.o -AppPublisherURL=http://pype.club -AppSupportURL=http://pype.club -AppUpdatesURL=http://pype.club +AppPublisher=Ynput s.r.o +AppPublisherURL=https://ynput.io +AppSupportURL=https://ynput.io +AppUpdatesURL=https://ynput.io DefaultDirName={autopf}\{#MyAppName}\{#AppVer} UsePreviousAppDir=no DisableProgramGroupPage=yes From 6d9c0e30802db386476f5541732526abfa77265e Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 17 May 2023 15:46:34 +0100 Subject: [PATCH 047/147] Added setting for base file unit scale --- openpype/settings/defaults/project_settings/blender.json | 1 + .../schemas/projects_schema/schema_project_blender.json | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/openpype/settings/defaults/project_settings/blender.json b/openpype/settings/defaults/project_settings/blender.json index 20eec0c09d..0b3f38a40f 100644 --- a/openpype/settings/defaults/project_settings/blender.json +++ b/openpype/settings/defaults/project_settings/blender.json @@ -1,4 +1,5 @@ { + "base_file_unit_scale": 0.01, "imageio": { "ocio_config": { "enabled": false, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json index 725d9bfb08..00414b3210 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json @@ -5,6 +5,12 @@ "label": "Blender", "is_file": true, "children": [ + { + "key": "base_file_unit_scale", + "type": "number", + "label": "Base File Unit Scale", + "decimal": 2 + }, { "key": "imageio", "type": "dict", From 3350e0995f43094a93a2e2a54abba7b52a7916fe Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 17 May 2023 15:47:38 +0100 Subject: [PATCH 048/147] Set base unit scale when opening a file or creating a new one --- openpype/hosts/blender/api/pipeline.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/openpype/hosts/blender/api/pipeline.py b/openpype/hosts/blender/api/pipeline.py index c2aee1e653..02b1560e56 100644 --- a/openpype/hosts/blender/api/pipeline.py +++ b/openpype/hosts/blender/api/pipeline.py @@ -26,6 +26,8 @@ from openpype.lib import ( emit_event ) import openpype.hosts.blender +from openpype.settings import get_project_settings + HOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.blender.__file__)) PLUGINS_DIR = os.path.join(HOST_DIR, "plugins") @@ -122,12 +124,23 @@ def set_start_end_frames(): scene.render.resolution_y = resolution_y +def set_base_file_unit_scale(): + project = os.environ.get("AVALON_PROJECT") + settings = get_project_settings(project) + + unit_scale = settings.get("blender").get("base_file_unit_scale") + + bpy.context.scene.unit_settings.scale_length = unit_scale + + def on_new(): set_start_end_frames() + set_base_file_unit_scale() def on_open(): set_start_end_frames() + set_base_file_unit_scale() @bpy.app.handlers.persistent From d95299a31b1d8aae602ac06993f57ce14dd397ee Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 18 May 2023 10:37:38 +0100 Subject: [PATCH 049/147] Added settings to make optional the setting the unit scale --- openpype/hosts/blender/api/pipeline.py | 27 ++++++++++++++---- .../defaults/project_settings/blender.json | 6 +++- .../schema_project_blender.json | 28 ++++++++++++++++--- 3 files changed, 50 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/blender/api/pipeline.py b/openpype/hosts/blender/api/pipeline.py index 02b1560e56..3618c1f4c8 100644 --- a/openpype/hosts/blender/api/pipeline.py +++ b/openpype/hosts/blender/api/pipeline.py @@ -124,19 +124,34 @@ def set_start_end_frames(): scene.render.resolution_y = resolution_y -def set_base_file_unit_scale(): +def on_new(): + set_start_end_frames() + project = os.environ.get("AVALON_PROJECT") settings = get_project_settings(project) - unit_scale = settings.get("blender").get("base_file_unit_scale") - - bpy.context.scene.unit_settings.scale_length = unit_scale + unit_scale_settings = settings.get("blender").get("unit_scale_settings") + unit_scale_enabled = unit_scale_settings.get("enabled") + if unit_scale_enabled: + unit_scale = unit_scale_settings.get("base_file_unit_scale") + bpy.context.scene.unit_settings.scale_length = unit_scale -def on_new(): +def on_open(): set_start_end_frames() - set_base_file_unit_scale() + project = os.environ.get("AVALON_PROJECT") + settings = get_project_settings(project) + + unit_scale_settings = settings.get("blender").get("unit_scale_settings") + unit_scale_enabled = unit_scale_settings.get("enabled") + apply_on_opening = unit_scale_settings.get("apply_on_opening") + if unit_scale_enabled and apply_on_opening: + unit_scale = unit_scale_settings.get("base_file_unit_scale") + prev_unit_scale = bpy.context.scene.unit_settings.scale_length + + if unit_scale != prev_unit_scale: + bpy.context.scene.unit_settings.scale_length = unit_scale def on_open(): set_start_end_frames() diff --git a/openpype/settings/defaults/project_settings/blender.json b/openpype/settings/defaults/project_settings/blender.json index 0b3f38a40f..41aebfa537 100644 --- a/openpype/settings/defaults/project_settings/blender.json +++ b/openpype/settings/defaults/project_settings/blender.json @@ -1,5 +1,9 @@ { - "base_file_unit_scale": 0.01, + "unit_scale_settings": { + "enabled": true, + "apply_on_opening": false, + "base_file_unit_scale": 0.01 + }, "imageio": { "ocio_config": { "enabled": false, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json index 00414b3210..0d0952a70a 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json @@ -6,10 +6,30 @@ "is_file": true, "children": [ { - "key": "base_file_unit_scale", - "type": "number", - "label": "Base File Unit Scale", - "decimal": 2 + "key": "unit_scale_settings", + "type": "dict", + "label": "Set Unit Scale", + "collapsible": true, + "is_group": true, + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "key": "apply_on_opening", + "type": "boolean", + "label": "Apply on Opening Existing Files" + }, + { + "key": "base_file_unit_scale", + "type": "number", + "label": "Base File Unit Scale", + "decimal": 2 + } + ] }, { "key": "imageio", From 0d4ae4efa7668ad5b5dc2a5a991b3e5cfcd92bf3 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 18 May 2023 10:38:22 +0100 Subject: [PATCH 050/147] Added message if base scale has been changed when opening a file --- openpype/hosts/blender/api/pipeline.py | 31 +++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/blender/api/pipeline.py b/openpype/hosts/blender/api/pipeline.py index 3618c1f4c8..9cc557c01a 100644 --- a/openpype/hosts/blender/api/pipeline.py +++ b/openpype/hosts/blender/api/pipeline.py @@ -85,6 +85,31 @@ def uninstall(): ops.unregister() +def show_message(title, message): + from openpype.widgets.message_window import Window + from .ops import BlenderApplication + + BlenderApplication.get_app() + + Window( + parent=None, + title=title, + message=message, + level="warning") + + +def message_window(title, message): + from .ops import ( + MainThreadItem, + execute_in_main_thread, + _process_app_events + ) + + mti = MainThreadItem(show_message, title, message) + execute_in_main_thread(mti) + _process_app_events() + + def set_start_end_frames(): project_name = legacy_io.active_project() asset_name = legacy_io.Session["AVALON_ASSET"] @@ -153,9 +178,9 @@ def on_open(): if unit_scale != prev_unit_scale: bpy.context.scene.unit_settings.scale_length = unit_scale -def on_open(): - set_start_end_frames() - set_base_file_unit_scale() + message_window( + "Base file unit scale changed", + "Base file unit scale changed to match the project settings.") @bpy.app.handlers.persistent From 8b68371e0c399b6e8b80f5300a77223bfefc7007 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 18 May 2023 14:33:26 +0100 Subject: [PATCH 051/147] Increased the number of decimals for the unit scale --- .../schemas/projects_schema/schema_project_blender.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json index 0d0952a70a..5b40169872 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json @@ -27,7 +27,7 @@ "key": "base_file_unit_scale", "type": "number", "label": "Base File Unit Scale", - "decimal": 2 + "decimal": 10 } ] }, From ec7c172fb2ec82d43ae014381829275581cf0ed2 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 18 May 2023 15:26:12 +0100 Subject: [PATCH 052/147] Implement loading of abc camera --- .../blender/plugins/load/load_camera_abc.py | 209 ++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 openpype/hosts/blender/plugins/load/load_camera_abc.py diff --git a/openpype/hosts/blender/plugins/load/load_camera_abc.py b/openpype/hosts/blender/plugins/load/load_camera_abc.py new file mode 100644 index 0000000000..21b48f409f --- /dev/null +++ b/openpype/hosts/blender/plugins/load/load_camera_abc.py @@ -0,0 +1,209 @@ +"""Load an asset in Blender from an Alembic file.""" + +from pathlib import Path +from pprint import pformat +from typing import Dict, List, Optional + +import bpy + +from openpype.pipeline import ( + get_representation_path, + AVALON_CONTAINER_ID, +) +from openpype.hosts.blender.api import plugin, lib +from openpype.hosts.blender.api.pipeline import ( + AVALON_CONTAINERS, + AVALON_PROPERTY, +) + + +class AbcCameraLoader(plugin.AssetLoader): + """Load a camera from Alembic file. + + Stores the imported asset in an empty named after the asset. + """ + + families = ["camera"] + representations = ["abc"] + + label = "Load Camera (ABC)" + icon = "code-fork" + color = "orange" + + def _remove(self, asset_group): + objects = list(asset_group.children) + + for obj in objects: + if obj.type == "CAMERA": + bpy.data.cameras.remove(obj.data) + elif obj.type == "EMPTY": + objects.extend(obj.children) + bpy.data.objects.remove(obj) + + def _process(self, libpath, asset_group, group_name): + plugin.deselect_all() + + bpy.ops.wm.alembic_import(filepath=libpath) + + objects = lib.get_selection() + + for obj in objects: + obj.parent = asset_group + + for obj in objects: + name = obj.name + obj.name = f"{group_name}:{name}" + if obj.type != "EMPTY": + name_data = obj.data.name + obj.data.name = f"{group_name}:{name_data}" + + if not obj.get(AVALON_PROPERTY): + obj[AVALON_PROPERTY] = dict() + + avalon_info = obj[AVALON_PROPERTY] + avalon_info.update({"container_name": group_name}) + + plugin.deselect_all() + + return objects + + def process_asset( + self, + context: dict, + name: str, + namespace: Optional[str] = None, + options: Optional[Dict] = None, + ) -> Optional[List]: + """ + Arguments: + name: Use pre-defined name + namespace: Use pre-defined namespace + context: Full parenthood of representation to load + options: Additional settings dictionary + """ + libpath = self.fname + asset = context["asset"]["name"] + subset = context["subset"]["name"] + + asset_name = plugin.asset_name(asset, subset) + unique_number = plugin.get_unique_number(asset, subset) + group_name = plugin.asset_name(asset, subset, unique_number) + namespace = namespace or f"{asset}_{unique_number}" + + avalon_container = bpy.data.collections.get(AVALON_CONTAINERS) + if not avalon_container: + avalon_container = bpy.data.collections.new(name=AVALON_CONTAINERS) + bpy.context.scene.collection.children.link(avalon_container) + + asset_group = bpy.data.objects.new(group_name, object_data=None) + avalon_container.objects.link(asset_group) + + objects = self._process(libpath, asset_group, group_name) + + objects = [] + nodes = list(asset_group.children) + + for obj in nodes: + objects.append(obj) + nodes.extend(list(obj.children)) + + bpy.context.scene.collection.objects.link(asset_group) + + asset_group[AVALON_PROPERTY] = { + "schema": "openpype:container-2.0", + "id": AVALON_CONTAINER_ID, + "name": name, + "namespace": namespace or "", + "loader": str(self.__class__.__name__), + "representation": str(context["representation"]["_id"]), + "libpath": libpath, + "asset_name": asset_name, + "parent": str(context["representation"]["parent"]), + "family": context["representation"]["context"]["family"], + "objectName": group_name, + } + + self[:] = objects + return objects + + def exec_update(self, container: Dict, representation: Dict): + """Update the loaded asset. + + This will remove all objects of the current collection, load the new + ones and add them to the collection. + If the objects of the collection are used in another collection they + will not be removed, only unlinked. Normally this should not be the + case though. + + Warning: + No nested collections are supported at the moment! + """ + object_name = container["objectName"] + asset_group = bpy.data.objects.get(object_name) + libpath = Path(get_representation_path(representation)) + extension = libpath.suffix.lower() + + self.log.info( + "Container: %s\nRepresentation: %s", + pformat(container, indent=2), + pformat(representation, indent=2), + ) + + assert asset_group, ( + f"The asset is not loaded: {container['objectName']}") + assert libpath, ( + f"No existing library file found for {container['objectName']}") + assert libpath.is_file(), f"The file doesn't exist: {libpath}" + assert extension in plugin.VALID_EXTENSIONS, ( + f"Unsupported file: {libpath}") + + metadata = asset_group.get(AVALON_PROPERTY) + group_libpath = metadata["libpath"] + + normalized_group_libpath = str( + Path(bpy.path.abspath(group_libpath)).resolve()) + normalized_libpath = str( + Path(bpy.path.abspath(str(libpath))).resolve()) + self.log.debug( + "normalized_group_libpath:\n %s\nnormalized_libpath:\n %s", + normalized_group_libpath, + normalized_libpath, + ) + if normalized_group_libpath == normalized_libpath: + self.log.info("Library already loaded, not updating...") + return + + mat = asset_group.matrix_basis.copy() + + self._remove(asset_group) + self._process(str(libpath), asset_group, object_name) + + asset_group.matrix_basis = mat + + metadata["libpath"] = str(libpath) + metadata["representation"] = str(representation["_id"]) + + def exec_remove(self, container: Dict) -> bool: + """Remove an existing container from a Blender scene. + + Arguments: + container (openpype:container-1.0): Container to remove, + from `host.ls()`. + + Returns: + bool: Whether the container was deleted. + + Warning: + No nested collections are supported at the moment! + """ + object_name = container["objectName"] + asset_group = bpy.data.objects.get(object_name) + + if not asset_group: + return False + + self._remove(asset_group) + + bpy.data.objects.remove(asset_group) + + return True From 2e7b0b9d954c606220f8ad89fdf86549dc38e3ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Fri, 19 May 2023 18:20:03 +0200 Subject: [PATCH 053/147] fix raise --- openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py index 2d1b773c5f..525905f1ab 100644 --- a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py +++ b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py @@ -24,7 +24,7 @@ class AddPythonScriptToLaunchArgs(PreLaunchHook): # Test script path exists python_script_path = Path(python_script_path) if not python_script_path.exists(): - raise self.log.warning( + self.log.warning( f"Python script {python_script_path} doesn't exist. " "Skipped..." ) From dc7373408f8d586f854b99da7c9eb799441a3fec Mon Sep 17 00:00:00 2001 From: Felix David Date: Mon, 22 May 2023 10:39:30 +0200 Subject: [PATCH 054/147] continue if not python script path --- openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py index 525905f1ab..559e9ae0ce 100644 --- a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py +++ b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py @@ -28,6 +28,7 @@ class AddPythonScriptToLaunchArgs(PreLaunchHook): f"Python script {python_script_path} doesn't exist. " "Skipped..." ) + continue if "--" in self.launch_context.launch_args: # Insert before separator From fa74cae511070afb4edd87028ecbcffd5c7f6142 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 22 May 2023 16:55:39 +0100 Subject: [PATCH 055/147] Implemented creator, loader and extractor for Unreal Levels --- .../unreal/plugins/create/create_umap.py | 46 ++++++ .../hosts/unreal/plugins/load/load_umap.py | 140 ++++++++++++++++++ .../publish/collect_instance_members.py | 2 +- .../unreal/plugins/publish/extract_umap.py | 48 ++++++ 4 files changed, 235 insertions(+), 1 deletion(-) create mode 100644 openpype/hosts/unreal/plugins/create/create_umap.py create mode 100644 openpype/hosts/unreal/plugins/load/load_umap.py create mode 100644 openpype/hosts/unreal/plugins/publish/extract_umap.py diff --git a/openpype/hosts/unreal/plugins/create/create_umap.py b/openpype/hosts/unreal/plugins/create/create_umap.py new file mode 100644 index 0000000000..34aa8cdc00 --- /dev/null +++ b/openpype/hosts/unreal/plugins/create/create_umap.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +from pathlib import Path + +import unreal + +from openpype.pipeline import CreatorError +from openpype.hosts.unreal.api.plugin import ( + UnrealAssetCreator, +) + + +class CreateUMap(UnrealAssetCreator): + """Create Level.""" + + identifier = "io.ayon.creators.unreal.umap" + label = "Level" + family = "uasset" + icon = "cube" + + def create(self, subset_name, instance_data, pre_create_data): + if pre_create_data.get("use_selection"): + ar = unreal.AssetRegistryHelpers.get_asset_registry() + + sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() + selection = [a.get_path_name() for a in sel_objects] + + if len(selection) != 1: + raise CreatorError("Please select only one object.") + + obj = selection[0] + + asset = ar.get_asset_by_object_path(obj).get_asset() + sys_path = unreal.SystemLibrary.get_system_path(asset) + + if not sys_path: + raise CreatorError( + f"{Path(obj).name} is not on the disk. Likely it needs to" + "be saved first.") + + if Path(sys_path).suffix != ".umap": + raise CreatorError(f"{Path(sys_path).name} is not a Level.") + + super(CreateUMap, self).create( + subset_name, + instance_data, + pre_create_data) diff --git a/openpype/hosts/unreal/plugins/load/load_umap.py b/openpype/hosts/unreal/plugins/load/load_umap.py new file mode 100644 index 0000000000..f467fe6b3b --- /dev/null +++ b/openpype/hosts/unreal/plugins/load/load_umap.py @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- +"""Load Level.""" +from pathlib import Path +import shutil + +from openpype.pipeline import ( + get_representation_path, + AYON_CONTAINER_ID +) +from openpype.hosts.unreal.api import plugin +from openpype.hosts.unreal.api import pipeline as unreal_pipeline +import unreal # noqa + + +class UMapLoader(plugin.Loader): + """Load Level.""" + + families = ["uasset"] + label = "Load Level" + representations = ["umap"] + icon = "cube" + color = "orange" + + def load(self, context, name, namespace, options): + """Load and containerise representation into Content Browser. + + Args: + context (dict): application context + name (str): subset name + namespace (str): in Unreal this is basically path to container. + This is not passed here, so namespace is set + by `containerise()` because only then we know + real path. + options (dict): Those would be data to be imprinted. This is not + used now, data are imprinted by `containerise()`. + + Returns: + list(str): list of container content + """ + + # Create directory for asset and Ayon container + root = "/Game/Ayon/Assets" + asset = context.get('asset').get('name') + suffix = "_CON" + asset_name = f"{asset}_{name}" if asset else f"{name}" + tools = unreal.AssetToolsHelpers().get_asset_tools() + asset_dir, container_name = tools.create_unique_asset_name( + f"{root}/{asset}/{name}", suffix="" + ) + + container_name += suffix + + unreal.EditorAssetLibrary.make_directory(asset_dir) + + destination_path = asset_dir.replace( + "/Game", + Path(unreal.Paths.project_content_dir()).as_posix(), + 1) + + shutil.copy(self.fname, f"{destination_path}/{name}.uasset") + + # Create Asset Container + unreal_pipeline.create_container( + container=container_name, path=asset_dir) + + data = { + "schema": "ayon:container-2.0", + "id": AYON_CONTAINER_ID, + "asset": asset, + "namespace": asset_dir, + "container_name": container_name, + "asset_name": asset_name, + "loader": str(self.__class__.__name__), + "representation": context["representation"]["_id"], + "parent": context["representation"]["parent"], + "family": context["representation"]["context"]["family"] + } + unreal_pipeline.imprint(f"{asset_dir}/{container_name}", data) + + asset_content = unreal.EditorAssetLibrary.list_assets( + asset_dir, recursive=True, include_folder=True + ) + + for a in asset_content: + unreal.EditorAssetLibrary.save_asset(a) + + return asset_content + + def update(self, container, representation): + ar = unreal.AssetRegistryHelpers.get_asset_registry() + + asset_dir = container["namespace"] + name = representation["context"]["subset"] + + destination_path = asset_dir.replace( + "/Game", + Path(unreal.Paths.project_content_dir()).as_posix(), + 1) + + asset_content = unreal.EditorAssetLibrary.list_assets( + asset_dir, recursive=False, include_folder=True + ) + + for asset in asset_content: + obj = ar.get_asset_by_object_path(asset).get_asset() + if obj.get_class().get_name() != 'AyonAssetContainer': + unreal.EditorAssetLibrary.delete_asset(asset) + + update_filepath = get_representation_path(representation) + + shutil.copy(update_filepath, f"{destination_path}/{name}.umap") + + container_path = f'{container["namespace"]}/{container["objectName"]}' + # update metadata + unreal_pipeline.imprint( + container_path, + { + "representation": str(representation["_id"]), + "parent": str(representation["parent"]) + }) + + asset_content = unreal.EditorAssetLibrary.list_assets( + asset_dir, recursive=True, include_folder=True + ) + + for a in asset_content: + unreal.EditorAssetLibrary.save_asset(a) + + def remove(self, container): + path = container["namespace"] + parent_path = Path(path).parent.as_posix() + + unreal.EditorAssetLibrary.delete_directory(path) + + asset_content = unreal.EditorAssetLibrary.list_assets( + parent_path, recursive=False + ) + + if len(asset_content) == 0: + unreal.EditorAssetLibrary.delete_directory(parent_path) diff --git a/openpype/hosts/unreal/plugins/publish/collect_instance_members.py b/openpype/hosts/unreal/plugins/publish/collect_instance_members.py index 46ca51ab7e..de10e7b119 100644 --- a/openpype/hosts/unreal/plugins/publish/collect_instance_members.py +++ b/openpype/hosts/unreal/plugins/publish/collect_instance_members.py @@ -24,7 +24,7 @@ class CollectInstanceMembers(pyblish.api.InstancePlugin): ar = unreal.AssetRegistryHelpers.get_asset_registry() inst_path = instance.data.get('instance_path') - inst_name = instance.data.get('objectName') + inst_name = inst_path.split('/')[-1] pub_instance = ar.get_asset_by_object_path( f"{inst_path}.{inst_name}").get_asset() diff --git a/openpype/hosts/unreal/plugins/publish/extract_umap.py b/openpype/hosts/unreal/plugins/publish/extract_umap.py new file mode 100644 index 0000000000..3812834430 --- /dev/null +++ b/openpype/hosts/unreal/plugins/publish/extract_umap.py @@ -0,0 +1,48 @@ +from pathlib import Path +import shutil + +import unreal + +from openpype.pipeline import publish + + +class ExtractUMap(publish.Extractor): + """Extract a UMap.""" + + label = "Extract Level" + hosts = ["unreal"] + families = ["uasset"] + optional = True + + def process(self, instance): + ar = unreal.AssetRegistryHelpers.get_asset_registry() + + self.log.info("Performing extraction..") + + staging_dir = self.staging_dir(instance) + filename = f"{instance.name}.umap" + + members = instance.data.get("members", []) + + if not members: + raise RuntimeError("No members found in instance.") + + # UAsset publishing supports only one member + obj = members[0] + + asset = ar.get_asset_by_object_path(obj).get_asset() + sys_path = unreal.SystemLibrary.get_system_path(asset) + filename = Path(sys_path).name + + shutil.copy(sys_path, staging_dir) + + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': 'umap', + 'ext': 'umap', + 'files': filename, + "stagingDir": staging_dir, + } + instance.data["representations"].append(representation) From 14360f02176b3eb5fbb5a1bc9ba7ab54e33e6bd0 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 22 May 2023 18:11:13 +0100 Subject: [PATCH 056/147] Changed name and path of the camera levels to fix problem with sequencer --- openpype/hosts/unreal/api/__init__.py | 4 + openpype/hosts/unreal/api/pipeline.py | 130 ++++++++++++ .../hosts/unreal/plugins/load/load_camera.py | 192 +++++++----------- .../hosts/unreal/plugins/load/load_layout.py | 152 ++------------ 4 files changed, 228 insertions(+), 250 deletions(-) diff --git a/openpype/hosts/unreal/api/__init__.py b/openpype/hosts/unreal/api/__init__.py index de0fce13d5..ac6a91eae9 100644 --- a/openpype/hosts/unreal/api/__init__.py +++ b/openpype/hosts/unreal/api/__init__.py @@ -22,6 +22,8 @@ from .pipeline import ( show_tools_popup, instantiate, UnrealHost, + set_sequence_hierarchy, + generate_sequence, maintained_selection ) @@ -41,5 +43,7 @@ __all__ = [ "show_tools_popup", "instantiate", "UnrealHost", + "set_sequence_hierarchy", + "generate_sequence", "maintained_selection" ] diff --git a/openpype/hosts/unreal/api/pipeline.py b/openpype/hosts/unreal/api/pipeline.py index bb45fa8c01..5030e8ee86 100644 --- a/openpype/hosts/unreal/api/pipeline.py +++ b/openpype/hosts/unreal/api/pipeline.py @@ -9,12 +9,14 @@ import time import pyblish.api +from openpype.client import get_asset_by_name, get_assets from openpype.pipeline import ( register_loader_plugin_path, register_creator_plugin_path, deregister_loader_plugin_path, deregister_creator_plugin_path, AYON_CONTAINER_ID, + legacy_io, ) from openpype.tools.utils import host_tools import openpype.hosts.unreal @@ -512,6 +514,134 @@ def get_subsequences(sequence: unreal.LevelSequence): return [] +def set_sequence_hierarchy( + seq_i, seq_j, max_frame_i, min_frame_j, max_frame_j, map_paths +): + # Get existing sequencer tracks or create them if they don't exist + tracks = seq_i.get_master_tracks() + subscene_track = None + visibility_track = None + for t in tracks: + if t.get_class() == unreal.MovieSceneSubTrack.static_class(): + subscene_track = t + if (t.get_class() == + unreal.MovieSceneLevelVisibilityTrack.static_class()): + visibility_track = t + if not subscene_track: + subscene_track = seq_i.add_master_track(unreal.MovieSceneSubTrack) + if not visibility_track: + visibility_track = seq_i.add_master_track( + unreal.MovieSceneLevelVisibilityTrack) + + # Create the sub-scene section + subscenes = subscene_track.get_sections() + subscene = None + for s in subscenes: + if s.get_editor_property('sub_sequence') == seq_j: + subscene = s + break + if not subscene: + subscene = subscene_track.add_section() + subscene.set_row_index(len(subscene_track.get_sections())) + subscene.set_editor_property('sub_sequence', seq_j) + subscene.set_range( + min_frame_j, + max_frame_j + 1) + + # Create the visibility section + ar = unreal.AssetRegistryHelpers.get_asset_registry() + maps = [] + for m in map_paths: + # Unreal requires to load the level to get the map name + unreal.EditorLevelLibrary.save_all_dirty_levels() + unreal.EditorLevelLibrary.load_level(m) + maps.append(str(ar.get_asset_by_object_path(m).asset_name)) + + vis_section = visibility_track.add_section() + index = len(visibility_track.get_sections()) + + vis_section.set_range( + min_frame_j, + max_frame_j + 1) + vis_section.set_visibility(unreal.LevelVisibility.VISIBLE) + vis_section.set_row_index(index) + vis_section.set_level_names(maps) + + if min_frame_j > 1: + hid_section = visibility_track.add_section() + hid_section.set_range( + 1, + min_frame_j) + hid_section.set_visibility(unreal.LevelVisibility.HIDDEN) + hid_section.set_row_index(index) + hid_section.set_level_names(maps) + if max_frame_j < max_frame_i: + hid_section = visibility_track.add_section() + hid_section.set_range( + max_frame_j + 1, + max_frame_i + 1) + hid_section.set_visibility(unreal.LevelVisibility.HIDDEN) + hid_section.set_row_index(index) + hid_section.set_level_names(maps) + + +def generate_sequence(h, h_dir): + tools = unreal.AssetToolsHelpers().get_asset_tools() + + sequence = tools.create_asset( + asset_name=h, + package_path=h_dir, + asset_class=unreal.LevelSequence, + factory=unreal.LevelSequenceFactoryNew() + ) + + project_name = legacy_io.active_project() + asset_data = get_asset_by_name( + project_name, + h_dir.split('/')[-1], + fields=["_id", "data.fps"] + ) + + start_frames = [] + end_frames = [] + + elements = list(get_assets( + project_name, + parent_ids=[asset_data["_id"]], + fields=["_id", "data.clipIn", "data.clipOut"] + )) + for e in elements: + start_frames.append(e.get('data').get('clipIn')) + end_frames.append(e.get('data').get('clipOut')) + + elements.extend(get_assets( + project_name, + parent_ids=[e["_id"]], + fields=["_id", "data.clipIn", "data.clipOut"] + )) + + min_frame = min(start_frames) + max_frame = max(end_frames) + + sequence.set_display_rate( + unreal.FrameRate(asset_data.get('data').get("fps"), 1.0)) + sequence.set_playback_start(min_frame) + sequence.set_playback_end(max_frame) + + tracks = sequence.get_master_tracks() + track = None + for t in tracks: + if (t.get_class() == + unreal.MovieSceneCameraCutTrack.static_class()): + track = t + break + if not track: + track = sequence.add_master_track( + unreal.MovieSceneCameraCutTrack) + + return sequence, (min_frame, max_frame) + + @contextmanager def maintained_selection(): """Stub to be either implemented or replaced. diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 1bd398349f..02634104e7 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -6,13 +6,18 @@ import unreal from unreal import EditorAssetLibrary from unreal import EditorLevelLibrary from unreal import EditorLevelUtils -from openpype.client import get_assets, get_asset_by_name +from openpype.client import get_asset_by_name from openpype.pipeline import ( AYON_CONTAINER_ID, legacy_io, ) from openpype.hosts.unreal.api import plugin -from openpype.hosts.unreal.api import pipeline as unreal_pipeline +from openpype.hosts.unreal.api.pipeline import ( + generate_sequence, + set_sequence_hierarchy, + create_container, + imprint, +) class CameraLoader(plugin.Loader): @@ -24,32 +29,6 @@ class CameraLoader(plugin.Loader): icon = "cube" color = "orange" - def _set_sequence_hierarchy( - self, seq_i, seq_j, min_frame_j, max_frame_j - ): - tracks = seq_i.get_master_tracks() - track = None - for t in tracks: - if t.get_class() == unreal.MovieSceneSubTrack.static_class(): - track = t - break - if not track: - track = seq_i.add_master_track(unreal.MovieSceneSubTrack) - - subscenes = track.get_sections() - subscene = None - for s in subscenes: - if s.get_editor_property('sub_sequence') == seq_j: - subscene = s - break - if not subscene: - subscene = track.add_section() - subscene.set_row_index(len(track.get_sections())) - subscene.set_editor_property('sub_sequence', seq_j) - subscene.set_range( - min_frame_j, - max_frame_j + 1) - def _import_camera( self, world, sequence, bindings, import_fbx_settings, import_filename ): @@ -156,9 +135,9 @@ class CameraLoader(plugin.Loader): if not EditorAssetLibrary.does_asset_exist(master_level): EditorLevelLibrary.new_level(f"{h_dir}/{h_asset}_map") - level = f"{asset_path_parent}/{asset}_map.{asset}_map" + level = f"{asset_dir}/{asset}_map_camera.{asset}_map_camera" if not EditorAssetLibrary.does_asset_exist(level): - EditorLevelLibrary.new_level(f"{asset_path_parent}/{asset}_map") + EditorLevelLibrary.new_level(f"{asset_dir}/{asset}_map_camera") EditorLevelLibrary.load_level(master_level) EditorLevelUtils.add_level_to_world( @@ -169,27 +148,13 @@ class CameraLoader(plugin.Loader): EditorLevelLibrary.save_all_dirty_levels() EditorLevelLibrary.load_level(level) - project_name = legacy_io.active_project() - # TODO refactor - # - Creating of hierarchy should be a function in unreal integration - # - it's used in multiple loaders but must not be loader's logic - # - hard to say what is purpose of the loop - # - variables does not match their meaning - # - why scene is stored to sequences? - # - asset documents vs. elements - # - cleanup variable names in whole function - # - e.g. 'asset', 'asset_name', 'asset_data', 'asset_doc' - # - really inefficient queries of asset documents - # - existing asset in scene is considered as "with correct values" - # - variable 'elements' is modified during it's loop # Get all the sequences in the hierarchy. It will create them, if # they don't exist. - sequences = [] frame_ranges = [] - i = 0 - for h in hierarchy_dir_list: + sequences = [] + for (h_dir, h) in zip(hierarchy_dir_list, hierarchy): root_content = EditorAssetLibrary.list_assets( - h, recursive=False, include_folder=False) + h_dir, recursive=False, include_folder=False) existing_sequences = [ EditorAssetLibrary.find_asset_data(asset) @@ -199,48 +164,10 @@ class CameraLoader(plugin.Loader): ] if not existing_sequences: - scene = tools.create_asset( - asset_name=hierarchy[i], - package_path=h, - asset_class=unreal.LevelSequence, - factory=unreal.LevelSequenceFactoryNew() - ) + sequence, frame_range = generate_sequence(h, h_dir) - asset_data = get_asset_by_name( - project_name, - h.split('/')[-1], - fields=["_id", "data.fps"] - ) - - start_frames = [] - end_frames = [] - - elements = list(get_assets( - project_name, - parent_ids=[asset_data["_id"]], - fields=["_id", "data.clipIn", "data.clipOut"] - )) - - for e in elements: - start_frames.append(e.get('data').get('clipIn')) - end_frames.append(e.get('data').get('clipOut')) - - elements.extend(get_assets( - project_name, - parent_ids=[e["_id"]], - fields=["_id", "data.clipIn", "data.clipOut"] - )) - - min_frame = min(start_frames) - max_frame = max(end_frames) - - scene.set_display_rate( - unreal.FrameRate(asset_data.get('data').get("fps"), 1.0)) - scene.set_playback_start(min_frame) - scene.set_playback_end(max_frame) - - sequences.append(scene) - frame_ranges.append((min_frame, max_frame)) + sequences.append(sequence) + frame_ranges.append(frame_range) else: for e in existing_sequences: sequences.append(e.get_asset()) @@ -248,8 +175,6 @@ class CameraLoader(plugin.Loader): e.get_asset().get_playback_start(), e.get_asset().get_playback_end())) - i += 1 - EditorAssetLibrary.make_directory(asset_dir) cam_seq = tools.create_asset( @@ -260,19 +185,24 @@ class CameraLoader(plugin.Loader): ) # Add sequences data to hierarchy - for i in range(0, len(sequences) - 1): - self._set_sequence_hierarchy( + for i in range(len(sequences) - 1): + set_sequence_hierarchy( sequences[i], sequences[i + 1], - frame_ranges[i + 1][0], frame_ranges[i + 1][1]) + frame_ranges[i][1], + frame_ranges[i + 1][0], frame_ranges[i + 1][1], + [level]) + project_name = legacy_io.active_project() data = get_asset_by_name(project_name, asset)["data"] cam_seq.set_display_rate( unreal.FrameRate(data.get("fps"), 1.0)) cam_seq.set_playback_start(data.get('clipIn')) cam_seq.set_playback_end(data.get('clipOut') + 1) - self._set_sequence_hierarchy( + set_sequence_hierarchy( sequences[-1], cam_seq, - data.get('clipIn'), data.get('clipOut')) + frame_ranges[-1][1], + data.get('clipIn'), data.get('clipOut'), + [level]) settings = unreal.MovieSceneUserImportFBXSettings() settings.set_editor_property('reduce_keys', False) @@ -307,7 +237,7 @@ class CameraLoader(plugin.Loader): key.set_time(unreal.FrameNumber(value=new_time)) # Create Asset Container - unreal_pipeline.create_container( + create_container( container=container_name, path=asset_dir) data = { @@ -322,7 +252,7 @@ class CameraLoader(plugin.Loader): "parent": context["representation"]["parent"], "family": context["representation"]["context"]["family"] } - unreal_pipeline.imprint( + imprint( "{}/{}".format(asset_dir, container_name), data) EditorLevelLibrary.save_all_dirty_levels() @@ -360,7 +290,7 @@ class CameraLoader(plugin.Loader): sequences = ar.get_assets(filter) filter = unreal.ARFilter( class_names=["World"], - package_paths=[str(Path(asset_dir).parent.as_posix())], + package_paths=[asset_dir], recursive_paths=True) maps = ar.get_assets(filter) @@ -470,7 +400,7 @@ class CameraLoader(plugin.Loader): "representation": str(representation["_id"]), "parent": str(representation["parent"]) } - unreal_pipeline.imprint( + imprint( "{}/{}".format(asset_dir, container.get('container_name')), data) EditorLevelLibrary.save_current_level() @@ -484,15 +414,15 @@ class CameraLoader(plugin.Loader): EditorLevelLibrary.load_level(master_level) def remove(self, container): - path = Path(container.get("namespace")) - parent_path = str(path.parent.as_posix()) + asset_dir = container.get('namespace') + path = Path(asset_dir) ar = unreal.AssetRegistryHelpers.get_asset_registry() - filter = unreal.ARFilter( + _filter = unreal.ARFilter( class_names=["LevelSequence"], - package_paths=[f"{str(path.as_posix())}"], + package_paths=[asset_dir], recursive_paths=False) - sequences = ar.get_assets(filter) + sequences = ar.get_assets(_filter) if not sequences: raise Exception("Could not find sequence.") @@ -500,11 +430,11 @@ class CameraLoader(plugin.Loader): world = ar.get_asset_by_object_path( EditorLevelLibrary.get_editor_world().get_path_name()) - filter = unreal.ARFilter( + _filter = unreal.ARFilter( class_names=["World"], - package_paths=[f"{parent_path}"], + package_paths=[asset_dir], recursive_paths=True) - maps = ar.get_assets(filter) + maps = ar.get_assets(_filter) # There should be only one map in the list if not maps: @@ -534,12 +464,18 @@ class CameraLoader(plugin.Loader): root = "/Game/Ayon" namespace = container.get('namespace').replace(f"{root}/", "") ms_asset = namespace.split('/')[0] - filter = unreal.ARFilter( + _filter = unreal.ARFilter( class_names=["LevelSequence"], package_paths=[f"{root}/{ms_asset}"], recursive_paths=False) - sequences = ar.get_assets(filter) + sequences = ar.get_assets(_filter) master_sequence = sequences[0].get_asset() + _filter = unreal.ARFilter( + class_names=["World"], + package_paths=[f"{root}/{ms_asset}"], + recursive_paths=False) + levels = ar.get_assets(_filter) + master_level = levels[0].get_full_name() sequences = [master_sequence] @@ -547,10 +483,13 @@ class CameraLoader(plugin.Loader): for s in sequences: tracks = s.get_master_tracks() subscene_track = None + visibility_track = None for t in tracks: if t.get_class() == unreal.MovieSceneSubTrack.static_class(): subscene_track = t - break + if (t.get_class() == + unreal.MovieSceneLevelVisibilityTrack.static_class()): + visibility_track = t if subscene_track: sections = subscene_track.get_sections() for ss in sections: @@ -565,18 +504,45 @@ class CameraLoader(plugin.Loader): ss.set_row_index(i) i += 1 + if visibility_track: + sections = visibility_track.get_sections() + for ss in sections: + if (unreal.Name(f"{container.get('asset')}_map_camera") + in ss.get_level_names()): + visibility_track.remove_section(ss) + # Update visibility sections indexes. + i = -1 + prev_name = [] + for ss in sections: + if prev_name != ss.get_level_names(): + i += 1 + ss.set_row_index(i) + prev_name = ss.get_level_names() if parent: break assert parent, "Could not find the parent sequence" - EditorAssetLibrary.delete_directory(str(path.as_posix())) + # Create a temporary level to delete the layout level. + EditorLevelLibrary.save_all_dirty_levels() + EditorAssetLibrary.make_directory(f"{root}/tmp") + tmp_level = f"{root}/tmp/temp_map" + if not EditorAssetLibrary.does_asset_exist(f"{tmp_level}.temp_map"): + EditorLevelLibrary.new_level(tmp_level) + else: + EditorLevelLibrary.load_level(tmp_level) + + # Delete the layout directory. + EditorAssetLibrary.delete_directory(asset_dir) + + EditorLevelLibrary.load_level(master_level) + EditorAssetLibrary.delete_directory(f"{root}/tmp") # Check if there isn't any more assets in the parent folder, and # delete it if not. asset_content = EditorAssetLibrary.list_assets( - parent_path, recursive=False, include_folder=True + path.parent.as_posix(), recursive=False, include_folder=True ) if len(asset_content) == 0: - EditorAssetLibrary.delete_directory(parent_path) + EditorAssetLibrary.delete_directory(path.parent.as_posix()) diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index e5f32c3412..1dfe6f1f5c 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -13,7 +13,7 @@ from unreal import FBXImportType from unreal import MovieSceneLevelVisibilityTrack from unreal import MovieSceneSubTrack -from openpype.client import get_asset_by_name, get_assets, get_representations +from openpype.client import get_asset_by_name, get_representations from openpype.pipeline import ( discover_loader_plugins, loaders_from_representation, @@ -25,7 +25,13 @@ from openpype.pipeline import ( from openpype.pipeline.context_tools import get_current_project_asset from openpype.settings import get_current_project_settings from openpype.hosts.unreal.api import plugin -from openpype.hosts.unreal.api import pipeline as unreal_pipeline +from openpype.hosts.unreal.api.pipeline import ( + generate_sequence, + set_sequence_hierarchy, + create_container, + imprint, + ls, +) class LayoutLoader(plugin.Loader): @@ -91,77 +97,6 @@ class LayoutLoader(plugin.Loader): return None - @staticmethod - def _set_sequence_hierarchy( - seq_i, seq_j, max_frame_i, min_frame_j, max_frame_j, map_paths - ): - # Get existing sequencer tracks or create them if they don't exist - tracks = seq_i.get_master_tracks() - subscene_track = None - visibility_track = None - for t in tracks: - if t.get_class() == unreal.MovieSceneSubTrack.static_class(): - subscene_track = t - if (t.get_class() == - unreal.MovieSceneLevelVisibilityTrack.static_class()): - visibility_track = t - if not subscene_track: - subscene_track = seq_i.add_master_track(unreal.MovieSceneSubTrack) - if not visibility_track: - visibility_track = seq_i.add_master_track( - unreal.MovieSceneLevelVisibilityTrack) - - # Create the sub-scene section - subscenes = subscene_track.get_sections() - subscene = None - for s in subscenes: - if s.get_editor_property('sub_sequence') == seq_j: - subscene = s - break - if not subscene: - subscene = subscene_track.add_section() - subscene.set_row_index(len(subscene_track.get_sections())) - subscene.set_editor_property('sub_sequence', seq_j) - subscene.set_range( - min_frame_j, - max_frame_j + 1) - - # Create the visibility section - ar = unreal.AssetRegistryHelpers.get_asset_registry() - maps = [] - for m in map_paths: - # Unreal requires to load the level to get the map name - EditorLevelLibrary.save_all_dirty_levels() - EditorLevelLibrary.load_level(m) - maps.append(str(ar.get_asset_by_object_path(m).asset_name)) - - vis_section = visibility_track.add_section() - index = len(visibility_track.get_sections()) - - vis_section.set_range( - min_frame_j, - max_frame_j + 1) - vis_section.set_visibility(unreal.LevelVisibility.VISIBLE) - vis_section.set_row_index(index) - vis_section.set_level_names(maps) - - if min_frame_j > 1: - hid_section = visibility_track.add_section() - hid_section.set_range( - 1, - min_frame_j) - hid_section.set_visibility(unreal.LevelVisibility.HIDDEN) - hid_section.set_row_index(index) - hid_section.set_level_names(maps) - if max_frame_j < max_frame_i: - hid_section = visibility_track.add_section() - hid_section.set_range( - max_frame_j + 1, - max_frame_i + 1) - hid_section.set_visibility(unreal.LevelVisibility.HIDDEN) - hid_section.set_row_index(index) - hid_section.set_level_names(maps) - def _transform_from_basis(self, transform, basis): """Transform a transform from a basis to a new basis.""" # Get the basis matrix @@ -352,63 +287,6 @@ class LayoutLoader(plugin.Loader): sec_params = section.get_editor_property('params') sec_params.set_editor_property('animation', animation) - @staticmethod - def _generate_sequence(h, h_dir): - tools = unreal.AssetToolsHelpers().get_asset_tools() - - sequence = tools.create_asset( - asset_name=h, - package_path=h_dir, - asset_class=unreal.LevelSequence, - factory=unreal.LevelSequenceFactoryNew() - ) - - project_name = legacy_io.active_project() - asset_data = get_asset_by_name( - project_name, - h_dir.split('/')[-1], - fields=["_id", "data.fps"] - ) - - start_frames = [] - end_frames = [] - - elements = list(get_assets( - project_name, - parent_ids=[asset_data["_id"]], - fields=["_id", "data.clipIn", "data.clipOut"] - )) - for e in elements: - start_frames.append(e.get('data').get('clipIn')) - end_frames.append(e.get('data').get('clipOut')) - - elements.extend(get_assets( - project_name, - parent_ids=[e["_id"]], - fields=["_id", "data.clipIn", "data.clipOut"] - )) - - min_frame = min(start_frames) - max_frame = max(end_frames) - - sequence.set_display_rate( - unreal.FrameRate(asset_data.get('data').get("fps"), 1.0)) - sequence.set_playback_start(min_frame) - sequence.set_playback_end(max_frame) - - tracks = sequence.get_master_tracks() - track = None - for t in tracks: - if (t.get_class() == - unreal.MovieSceneCameraCutTrack.static_class()): - track = t - break - if not track: - track = sequence.add_master_track( - unreal.MovieSceneCameraCutTrack) - - return sequence, (min_frame, max_frame) - def _get_repre_docs_by_version_id(self, data): version_ids = { element.get("version") @@ -696,7 +574,7 @@ class LayoutLoader(plugin.Loader): ] if not existing_sequences: - sequence, frame_range = self._generate_sequence(h, h_dir) + sequence, frame_range = generate_sequence(h, h_dir) sequences.append(sequence) frame_ranges.append(frame_range) @@ -716,7 +594,7 @@ class LayoutLoader(plugin.Loader): # sequences and frame_ranges have the same length for i in range(0, len(sequences) - 1): - self._set_sequence_hierarchy( + set_sequence_hierarchy( sequences[i], sequences[i + 1], frame_ranges[i][1], frame_ranges[i + 1][0], frame_ranges[i + 1][1], @@ -729,7 +607,7 @@ class LayoutLoader(plugin.Loader): shot.set_playback_start(0) shot.set_playback_end(data.get('clipOut') - data.get('clipIn') + 1) if sequences: - self._set_sequence_hierarchy( + set_sequence_hierarchy( sequences[-1], shot, frame_ranges[-1][1], data.get('clipIn'), data.get('clipOut'), @@ -745,7 +623,7 @@ class LayoutLoader(plugin.Loader): EditorLevelLibrary.save_current_level() # Create Asset Container - unreal_pipeline.create_container( + create_container( container=container_name, path=asset_dir) data = { @@ -761,7 +639,7 @@ class LayoutLoader(plugin.Loader): "family": context["representation"]["context"]["family"], "loaded_assets": loaded_assets } - unreal_pipeline.imprint( + imprint( "{}/{}".format(asset_dir, container_name), data) asset_content = EditorAssetLibrary.list_assets( @@ -843,7 +721,7 @@ class LayoutLoader(plugin.Loader): "parent": str(representation["parent"]), "loaded_assets": loaded_assets } - unreal_pipeline.imprint( + imprint( "{}/{}".format(asset_dir, container.get('container_name')), data) EditorLevelLibrary.save_current_level() @@ -870,7 +748,7 @@ class LayoutLoader(plugin.Loader): root = "/Game/Ayon" path = Path(container.get("namespace")) - containers = unreal_pipeline.ls() + containers = ls() layout_containers = [ c for c in containers if (c.get('asset_name') != container.get('asset_name') and From 4adf8388b4c08738c99b3f4fd8dfb778fb841dee Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 23 May 2023 09:17:54 +0100 Subject: [PATCH 057/147] Fix problem with updating camera --- .../hosts/unreal/plugins/load/load_camera.py | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 02634104e7..9361ed2ac5 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -345,17 +345,21 @@ class CameraLoader(plugin.Loader): for s in sequences: tracks = s.get_master_tracks() subscene_track = None + visibility_track = None for t in tracks: if t.get_class() == unreal.MovieSceneSubTrack.static_class(): subscene_track = t - break + if (t.get_class() == + unreal.MovieSceneLevelVisibilityTrack.static_class()): + visibility_track = t if subscene_track: sections = subscene_track.get_sections() for ss in sections: - if ss.get_sequence().get_name() == sequence_name: + if (ss.get_sequence().get_name() == + container.get('asset')): parent = s sub_scene = ss - # subscene_track.remove_section(ss) + subscene_track.remove_section(ss) break sequences.append(ss.get_sequence()) # Update subscenes indexes. @@ -364,10 +368,24 @@ class CameraLoader(plugin.Loader): ss.set_row_index(i) i += 1 + if visibility_track: + sections = visibility_track.get_sections() + for ss in sections: + if (unreal.Name(f"{container.get('asset')}_map") + in ss.get_level_names()): + visibility_track.remove_section(ss) + # Update visibility sections indexes. + i = -1 + prev_name = [] + for ss in sections: + if prev_name != ss.get_level_names(): + i += 1 + ss.set_row_index(i) + prev_name = ss.get_level_names() if parent: break - assert parent, "Could not find the parent sequence" + assert parent, "Could not find the parent sequence" EditorAssetLibrary.delete_asset(level_sequence.get_path_name()) From b7556f76d5cbc61841ec4593c99fa20dd42d7023 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 23 May 2023 10:23:14 +0100 Subject: [PATCH 058/147] Fix sequence when updating camera... again --- .../hosts/unreal/plugins/load/load_camera.py | 43 ++++--------------- 1 file changed, 9 insertions(+), 34 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 9361ed2ac5..6f20a466a9 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -270,7 +270,7 @@ class CameraLoader(plugin.Loader): def update(self, container, representation): ar = unreal.AssetRegistryHelpers.get_asset_registry() - root = "/Game/ayon" + root = "/Game/Ayon" asset_dir = container.get('namespace') @@ -283,16 +283,16 @@ class CameraLoader(plugin.Loader): EditorLevelLibrary.save_current_level() - filter = unreal.ARFilter( + _filter = unreal.ARFilter( class_names=["LevelSequence"], package_paths=[asset_dir], recursive_paths=False) - sequences = ar.get_assets(filter) - filter = unreal.ARFilter( + sequences = ar.get_assets(_filter) + _filter = unreal.ARFilter( class_names=["World"], package_paths=[asset_dir], recursive_paths=True) - maps = ar.get_assets(filter) + maps = ar.get_assets(_filter) # There should be only one map in the list EditorLevelLibrary.load_level(maps[0].get_full_name()) @@ -328,14 +328,13 @@ class CameraLoader(plugin.Loader): # Remove the Level Sequence from the parent. # We need to traverse the hierarchy from the master sequence to find # the level sequence. - root = "/Game/Ayon" namespace = container.get('namespace').replace(f"{root}/", "") ms_asset = namespace.split('/')[0] - filter = unreal.ARFilter( + _filter = unreal.ARFilter( class_names=["LevelSequence"], package_paths=[f"{root}/{ms_asset}"], recursive_paths=False) - sequences = ar.get_assets(filter) + sequences = ar.get_assets(_filter) master_sequence = sequences[0].get_asset() sequences = [master_sequence] @@ -345,43 +344,19 @@ class CameraLoader(plugin.Loader): for s in sequences: tracks = s.get_master_tracks() subscene_track = None - visibility_track = None for t in tracks: if t.get_class() == unreal.MovieSceneSubTrack.static_class(): subscene_track = t - if (t.get_class() == - unreal.MovieSceneLevelVisibilityTrack.static_class()): - visibility_track = t if subscene_track: sections = subscene_track.get_sections() for ss in sections: - if (ss.get_sequence().get_name() == - container.get('asset')): + if ss.get_sequence().get_name() == sequence_name: parent = s sub_scene = ss - subscene_track.remove_section(ss) break sequences.append(ss.get_sequence()) - # Update subscenes indexes. - i = 0 - for ss in sections: + for i, ss in enumerate(sections): ss.set_row_index(i) - i += 1 - - if visibility_track: - sections = visibility_track.get_sections() - for ss in sections: - if (unreal.Name(f"{container.get('asset')}_map") - in ss.get_level_names()): - visibility_track.remove_section(ss) - # Update visibility sections indexes. - i = -1 - prev_name = [] - for ss in sections: - if prev_name != ss.get_level_names(): - i += 1 - ss.set_row_index(i) - prev_name = ss.get_level_names() if parent: break From bf816562f09dc53510a8834e8e3c5ae9ba8b6597 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 23 May 2023 10:48:17 +0100 Subject: [PATCH 059/147] Fix old camera not being deleted on update --- .../hosts/unreal/plugins/load/load_camera.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 6f20a466a9..8fd6cc3e80 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -270,17 +270,8 @@ class CameraLoader(plugin.Loader): def update(self, container, representation): ar = unreal.AssetRegistryHelpers.get_asset_registry() - root = "/Game/Ayon" - asset_dir = container.get('namespace') - context = representation.get("context") - - hierarchy = context.get('hierarchy').split("/") - h_dir = f"{root}/{hierarchy[0]}" - h_asset = hierarchy[0] - master_level = f"{h_dir}/{h_asset}_map.{h_asset}_map" - EditorLevelLibrary.save_current_level() _filter = unreal.ARFilter( @@ -295,7 +286,7 @@ class CameraLoader(plugin.Loader): maps = ar.get_assets(_filter) # There should be only one map in the list - EditorLevelLibrary.load_level(maps[0].get_full_name()) + EditorLevelLibrary.load_level(maps[0].get_asset().get_path_name()) level_sequence = sequences[0].get_asset() @@ -328,6 +319,7 @@ class CameraLoader(plugin.Loader): # Remove the Level Sequence from the parent. # We need to traverse the hierarchy from the master sequence to find # the level sequence. + root = "/Game/Ayon" namespace = container.get('namespace').replace(f"{root}/", "") ms_asset = namespace.split('/')[0] _filter = unreal.ARFilter( @@ -336,6 +328,12 @@ class CameraLoader(plugin.Loader): recursive_paths=False) sequences = ar.get_assets(_filter) master_sequence = sequences[0].get_asset() + _filter = unreal.ARFilter( + class_names=["World"], + package_paths=[f"{root}/{ms_asset}"], + recursive_paths=False) + levels = ar.get_assets(_filter) + master_level = levels[0].get_asset().get_path_name() sequences = [master_sequence] From 902e6a9b61a7290e910c30009beb23dad9337c58 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 23 May 2023 15:54:11 +0100 Subject: [PATCH 060/147] Long names fixes. --- .../hosts/maya/plugins/publish/validate_rig_output_ids.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py b/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py index 499bfd4e37..cba70a21b7 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py @@ -55,7 +55,8 @@ class ValidateRigOutputIds(pyblish.api.InstancePlugin): if shapes: instance_nodes.extend(shapes) - scene_nodes = cmds.ls(type="transform") + cmds.ls(type="mesh") + scene_nodes = cmds.ls(type="transform", long=True) + scene_nodes += cmds.ls(type="mesh", long=True) scene_nodes = set(scene_nodes) - set(instance_nodes) scene_nodes_by_basename = defaultdict(list) @@ -76,7 +77,7 @@ class ValidateRigOutputIds(pyblish.api.InstancePlugin): if len(ids) > 1: cls.log.error( "\"{}\" id mismatch to: {}".format( - instance_node.longName(), matches + instance_node, matches ) ) invalid[instance_node] = matches From 96a4edf8cb412906047be7435a742ec80e2f4b94 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 23 May 2023 23:02:52 +0200 Subject: [PATCH 061/147] Resolve: fixing the issue with no active timeline during bootstrap of loader --- openpype/hosts/resolve/api/lib.py | 32 ++++++++++++++++--- .../hosts/resolve/plugins/load/load_clip.py | 1 + 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index b3ad20df39..1c33749a77 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -91,16 +91,39 @@ def get_current_project(): return self.project_manager.GetCurrentProject() -def get_current_timeline(new=False): +def get_current_timeline(any=False, new=False): + """Get current timeline object. + + Args: + any (bool, optional): return any even new if no timeline available. + Defaults to False. + new (bool, optional): return only new timeline. Defaults to False. + + Returns: + _type_: _description_ + """ # get current project project = get_current_project() + timeline = project.GetCurrentTimeline() + + # return current timeline only if it is not new + if timeline and not new: + return timeline + + # if any is True then return any timeline + if any: + timeline_count = project.GetTimelineCount() + if timeline_count == 0: + # if there is no timeline then create a new one + new = True + + # create new timeline if new is True if new: media_pool = project.GetMediaPool() new_timeline = media_pool.CreateEmptyTimeline(self.pype_timeline_name) project.SetCurrentTimeline(new_timeline) - - return project.GetCurrentTimeline() + return new_timeline def create_bin(name: str, root: object = None) -> object: @@ -312,7 +335,8 @@ def get_current_timeline_items( track_type = track_type or "video" selecting_color = selecting_color or "Chocolate" project = get_current_project() - timeline = get_current_timeline() + # make sure some timeline will be active with `any` argument + timeline = get_current_timeline(any=True) selected_clips = [] # get all tracks count filtered by track type diff --git a/openpype/hosts/resolve/plugins/load/load_clip.py b/openpype/hosts/resolve/plugins/load/load_clip.py index d30a7ea272..05bfb003d6 100644 --- a/openpype/hosts/resolve/plugins/load/load_clip.py +++ b/openpype/hosts/resolve/plugins/load/load_clip.py @@ -19,6 +19,7 @@ from openpype.lib.transcoding import ( IMAGE_EXTENSIONS ) + class LoadClip(plugin.TimelineItemLoader): """Load a subset to timeline as clip From b5827e8cdfcde48a7a9eff6aa29a21f48541ab66 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 24 May 2023 12:18:05 +0100 Subject: [PATCH 062/147] Fix section range on update camera --- .../hosts/unreal/plugins/load/load_camera.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index d198be29f4..c4fe9df70b 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -387,6 +387,30 @@ class CameraLoader(plugin.Loader): str(representation["data"]["path"]) ) + # Set range of all sections + # Changing the range of the section is not enough. We need to change + # the frame of all the keys in the section. + project_name = legacy_io.active_project() + asset = container.get('asset') + data = get_asset_by_name(project_name, asset)["data"] + + for possessable in new_sequence.get_possessables(): + for tracks in possessable.get_tracks(): + for section in tracks.get_sections(): + section.set_range( + data.get('clipIn'), + data.get('clipOut') + 1) + for channel in section.get_all_channels(): + for key in channel.get_keys(): + old_time = key.get_time().get_editor_property( + 'frame_number') + old_time_value = old_time.get_editor_property( + 'value') + new_time = old_time_value + ( + data.get('clipIn') - data.get('frameStart') + ) + key.set_time(unreal.FrameNumber(value=new_time)) + data = { "representation": str(representation["_id"]), "parent": str(representation["parent"]) From 7cfbb972a5ad41eea6e7bac6775ec8697edcb342 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 24 May 2023 12:40:28 +0100 Subject: [PATCH 063/147] Fix sequence frames validator to use correct data --- .../hosts/unreal/plugins/publish/validate_sequence_frames.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py b/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py index e6584e130f..76bb25fac3 100644 --- a/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py +++ b/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py @@ -31,8 +31,8 @@ class ValidateSequenceFrames(pyblish.api.InstancePlugin): frames = list(collection.indexes) current_range = (frames[0], frames[-1]) - required_range = (data["frameStart"], - data["frameEnd"]) + required_range = (data["clipIn"], + data["clipOut"]) if current_range != required_range: raise ValueError(f"Invalid frame range: {current_range} - " From 45e1dbc8410fb66dd7e685e77946db2952b35672 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 24 May 2023 13:14:46 +0100 Subject: [PATCH 064/147] Fix render instances collection to use correct data --- .../hosts/unreal/plugins/publish/collect_render_instances.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/unreal/plugins/publish/collect_render_instances.py b/openpype/hosts/unreal/plugins/publish/collect_render_instances.py index a352b2c3f3..dad0310dfc 100644 --- a/openpype/hosts/unreal/plugins/publish/collect_render_instances.py +++ b/openpype/hosts/unreal/plugins/publish/collect_render_instances.py @@ -103,8 +103,8 @@ class CollectRenderInstances(pyblish.api.InstancePlugin): new_instance.data["representations"] = [] repr = { - 'frameStart': s.get('frame_range')[0], - 'frameEnd': s.get('frame_range')[1], + 'frameStart': instance.data["frameStart"], + 'frameEnd': instance.data["frameEnd"], 'name': 'png', 'ext': 'png', 'files': frames, From d4212ef9918e805025fb93fdfdaf5b5fa82f2d7c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 24 May 2023 22:04:42 +0200 Subject: [PATCH 065/147] Return any timeline in case none is detected as active also adding in host test --- openpype/hosts/resolve/api/lib.py | 16 +++++++++------- .../utility_scripts/tests/testing_timeline_op.py | 13 +++++++++++++ 2 files changed, 22 insertions(+), 7 deletions(-) create mode 100644 openpype/hosts/resolve/utility_scripts/tests/testing_timeline_op.py diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index 1c33749a77..d42521200a 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -91,16 +91,16 @@ def get_current_project(): return self.project_manager.GetCurrentProject() -def get_current_timeline(any=False, new=False): +def get_current_timeline(new=False, get_any=False): """Get current timeline object. Args: - any (bool, optional): return any even new if no timeline available. - Defaults to False. new (bool, optional): return only new timeline. Defaults to False. + get_any (bool, optional): return any even new if no timeline available. + Defaults to False. Returns: - _type_: _description_ + object: resolve.Timeline """ # get current project project = get_current_project() @@ -111,12 +111,14 @@ def get_current_timeline(any=False, new=False): if timeline and not new: return timeline - # if any is True then return any timeline - if any: + # if get_any is True then return any timeline + if get_any: timeline_count = project.GetTimelineCount() if timeline_count == 0: # if there is no timeline then create a new one new = True + else: + return project.GetTimelineByIndex(1) # create new timeline if new is True if new: @@ -336,7 +338,7 @@ def get_current_timeline_items( selecting_color = selecting_color or "Chocolate" project = get_current_project() # make sure some timeline will be active with `any` argument - timeline = get_current_timeline(any=True) + timeline = get_current_timeline(get_any=True) selected_clips = [] # get all tracks count filtered by track type diff --git a/openpype/hosts/resolve/utility_scripts/tests/testing_timeline_op.py b/openpype/hosts/resolve/utility_scripts/tests/testing_timeline_op.py new file mode 100644 index 0000000000..8270496f64 --- /dev/null +++ b/openpype/hosts/resolve/utility_scripts/tests/testing_timeline_op.py @@ -0,0 +1,13 @@ +#! python3 +from openpype.pipeline import install_host +from openpype.hosts.resolve import api as bmdvr +from openpype.hosts.resolve.api.lib import get_current_project + +if __name__ == "__main__": + install_host(bmdvr) + project = get_current_project() + timeline_count = project.GetTimelineCount() + print(f"Timeline count: {timeline_count}") + timeline = project.GetTimelineByIndex(timeline_count) + print(f"Timeline name: {timeline.GetName()}") + print(timeline.GetTrackCount("video")) From 99a1be366e77db5549b294b8e37bb3089061cdd4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 24 May 2023 22:19:46 +0200 Subject: [PATCH 066/147] nuke: callback for dirmapping is on demand --- openpype/hosts/nuke/api/pipeline.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index d649ffae7f..75b0f80d21 100644 --- a/openpype/hosts/nuke/api/pipeline.py +++ b/openpype/hosts/nuke/api/pipeline.py @@ -151,6 +151,7 @@ class NukeHost( def add_nuke_callbacks(): """ Adding all available nuke callbacks """ + nuke_settings = get_current_project_settings()["nuke"] workfile_settings = WorkfileSettings() # Set context settings. nuke.addOnCreate( @@ -169,7 +170,10 @@ def add_nuke_callbacks(): # # set apply all workfile settings on script load and save nuke.addOnScriptLoad(WorkfileSettings().set_context_settings) - nuke.addFilenameFilter(dirmap_file_name_filter) + if nuke_settings["nuke-dirmap"]["enabled"]: + log.info("Added Nuke's dirmaping callback ...") + # Add dirmap for file paths. + nuke.addFilenameFilter(dirmap_file_name_filter) log.info("Added Nuke callbacks ...") From 41ae41d7512ebdc3270d49904f264cdde6c44f75 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 25 May 2023 10:00:29 +0200 Subject: [PATCH 067/147] Enhancement/publisher: Remove "hit play to continue" label on continue (#5029) * Clear message label on publish so that on "continue" it does not persist the "Hit play to continue" message * Clear main label on reset since the label isn't visible anyway * Remove unnecessary comment --------- Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/tools/publisher/widgets/publish_frame.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/tools/publisher/widgets/publish_frame.py b/openpype/tools/publisher/widgets/publish_frame.py index d21130deff..d423f97047 100644 --- a/openpype/tools/publisher/widgets/publish_frame.py +++ b/openpype/tools/publisher/widgets/publish_frame.py @@ -310,7 +310,7 @@ class PublishFrame(QtWidgets.QWidget): self._set_success_property() self._set_progress_visibility(True) - self._main_label.setText("Hit publish (play button)! If you want") + self._main_label.setText("") self._message_label_top.setText("") self._reset_btn.setEnabled(True) @@ -331,6 +331,7 @@ class PublishFrame(QtWidgets.QWidget): self._set_success_property(3) self._set_progress_visibility(True) self._set_main_label("Publishing...") + self._message_label_top.setText("") self._reset_btn.setEnabled(False) self._stop_btn.setEnabled(True) From 55040e6f74116b23531fa87f8965b83ab68d1316 Mon Sep 17 00:00:00 2001 From: Thomas Fricard <51854004+friquette@users.noreply.github.com> Date: Thu, 25 May 2023 10:32:36 +0200 Subject: [PATCH 068/147] Drop-down menu to list all families in create placeholder (#4928) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * convert family text field to enum field * get families from loaders and not creators * refactor the list families part * remove discover_loader_plugins call since there is already a variable with loaders plugins --------- Co-authored-by: Thomas Fricard Co-authored-by: ClΓ©ment Hector --- .../workfile/workfile_template_builder.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index a3d7340367..896ed40f2d 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -43,6 +43,7 @@ from openpype.pipeline.load import ( get_contexts_for_repre_docs, load_with_repre_context, ) + from openpype.pipeline.create import ( discover_legacy_creator_plugins, CreateContext, @@ -1246,6 +1247,16 @@ class PlaceholderLoadMixin(object): loader_items = list(sorted(loader_items, key=lambda i: i["label"])) options = options or {} + + # Get families from all loaders excluding "*" + families = set() + for loader in loaders_by_name.values(): + families.update(loader.families) + families.discard("*") + + # Sort for readability + families = list(sorted(families)) + return [ attribute_definitions.UISeparatorDef(), attribute_definitions.UILabelDef("Main attributes"), @@ -1272,11 +1283,11 @@ class PlaceholderLoadMixin(object): " field \"inputLinks\"" ) ), - attribute_definitions.TextDef( + attribute_definitions.EnumDef( "family", label="Family", default=options.get("family"), - placeholder="model, look, ..." + items=families ), attribute_definitions.TextDef( "representation", From 124493affd3e3b34563e35c83e34c7c66b5ee99b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 25 May 2023 11:51:44 +0200 Subject: [PATCH 069/147] Publisher: UI works with instances without label (#5032) * implemented helper function to get instance label * use 'get_publish_instance_label' in some of existing plugins * use 'get_publish_instance_label' in publisher controller --- openpype/pipeline/publish/__init__.py | 2 ++ openpype/pipeline/publish/lib.py | 32 +++++++++++++++++++ openpype/plugins/publish/extract_review.py | 16 +++------- .../plugins/publish/integrate_thumbnail.py | 12 ++----- openpype/tools/publisher/control.py | 3 +- 5 files changed, 44 insertions(+), 21 deletions(-) diff --git a/openpype/pipeline/publish/__init__.py b/openpype/pipeline/publish/__init__.py index 72f3774e1a..0c57915c05 100644 --- a/openpype/pipeline/publish/__init__.py +++ b/openpype/pipeline/publish/__init__.py @@ -39,6 +39,7 @@ from .lib import ( apply_plugin_settings_automatically, get_plugin_settings, + get_publish_instance_label, ) from .abstract_expected_files import ExpectedFiles @@ -85,6 +86,7 @@ __all__ = ( "apply_plugin_settings_automatically", "get_plugin_settings", + "get_publish_instance_label", "ExpectedFiles", diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index b55f813b5e..e87b865dce 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -30,6 +30,8 @@ from .contants import ( TRANSIENT_DIR_TEMPLATE ) +_ARG_PLACEHOLDER = object() + def get_template_name_profiles( project_name, project_settings=None, logger=None @@ -866,3 +868,33 @@ def add_repre_files_for_cleanup(instance, repre): for file_name in files: expected_file = os.path.join(staging_dir, file_name) instance.context.data["cleanupFullPaths"].append(expected_file) + + +def get_publish_instance_label(instance, default=_ARG_PLACEHOLDER): + """Try to get label from pyblish instance. + + First are checked 'label' and 'name' keys in instance data. If are not set + a default value is returned. Instance object is converted to string + if default value is not specific. + + Todos: + Maybe 'subset' key could be used too. + + Args: + instance (pyblish.api.Instance): Pyblish instance. + default (Optional[Any]): Default value to return if any + + Returns: + Union[Any]: Instance label or default label. + """ + + label = ( + instance.data.get("label") + or instance.data.get("name") + ) + if label: + return label + + if default is _ARG_PLACEHOLDER: + return str(instance) + return default diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index fa58c03df1..d04893fa7e 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -23,7 +23,10 @@ from openpype.lib.transcoding import ( convert_input_paths_for_ffmpeg, get_transcode_temp_directory, ) -from openpype.pipeline.publish import KnownPublishError +from openpype.pipeline.publish import ( + KnownPublishError, + get_publish_instance_label, +) from openpype.pipeline.publish.lib import add_repre_files_for_cleanup @@ -203,17 +206,8 @@ class ExtractReview(pyblish.api.InstancePlugin): return filtered_defs - @staticmethod - def get_instance_label(instance): - return ( - getattr(instance, "label", None) - or instance.data.get("label") - or instance.data.get("name") - or str(instance) - ) - def main_process(self, instance): - instance_label = self.get_instance_label(instance) + instance_label = get_publish_instance_label(instance) self.log.debug("Processing instance \"{}\"".format(instance_label)) profile_outputs = self._get_outputs_for_instance(instance) if not profile_outputs: diff --git a/openpype/plugins/publish/integrate_thumbnail.py b/openpype/plugins/publish/integrate_thumbnail.py index f6d4f654f5..2e87d8fc86 100644 --- a/openpype/plugins/publish/integrate_thumbnail.py +++ b/openpype/plugins/publish/integrate_thumbnail.py @@ -20,6 +20,7 @@ import pyblish.api from openpype.client import get_versions from openpype.client.operations import OperationsSession, new_thumbnail_doc +from openpype.pipeline.publish import get_publish_instance_label InstanceFilterResult = collections.namedtuple( "InstanceFilterResult", @@ -133,7 +134,7 @@ class IntegrateThumbnails(pyblish.api.ContextPlugin): filtered_instances = [] for instance in context: - instance_label = self._get_instance_label(instance) + instance_label = get_publish_instance_label(instance) # Skip instances without published representations # - there is no place where to put the thumbnail published_repres = instance.data.get("published_representations") @@ -248,7 +249,7 @@ class IntegrateThumbnails(pyblish.api.ContextPlugin): for instance_item in filtered_instance_items: instance, thumbnail_path, version_id = instance_item - instance_label = self._get_instance_label(instance) + instance_label = get_publish_instance_label(instance) version_doc = version_docs_by_str_id.get(version_id) if not version_doc: self.log.warning(( @@ -339,10 +340,3 @@ class IntegrateThumbnails(pyblish.api.ContextPlugin): )) op_session.commit() - - def _get_instance_label(self, instance): - return ( - instance.data.get("label") - or instance.data.get("name") - or "N/A" - ) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 8095d00103..89c2343ef7 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -40,6 +40,7 @@ from openpype.pipeline.create.context import ( CreatorsOperationFailed, ConvertorsOperationFailed, ) +from openpype.pipeline.publish import get_publish_instance_label # Define constant for plugin orders offset PLUGIN_ORDER_OFFSET = 0.5 @@ -346,7 +347,7 @@ class PublishReportMaker: def _extract_instance_data(self, instance, exists): return { "name": instance.data.get("name"), - "label": instance.data.get("label"), + "label": get_publish_instance_label(instance), "family": instance.data["family"], "families": instance.data.get("families") or [], "exists": exists, From 48b4934ee36628f57a895ff9e8cf7f349ddcea6d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 25 May 2023 13:33:11 +0200 Subject: [PATCH 070/147] limit number of ftrack events to query at once (#5033) --- openpype/modules/ftrack/ftrack_server/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/ftrack_server/lib.py b/openpype/modules/ftrack/ftrack_server/lib.py index eb64063fab..2226c85ef9 100644 --- a/openpype/modules/ftrack/ftrack_server/lib.py +++ b/openpype/modules/ftrack/ftrack_server/lib.py @@ -196,7 +196,7 @@ class ProcessEventHub(SocketBaseEventHub): {"pype_data.is_processed": False} ).sort( [("pype_data.stored", pymongo.ASCENDING)] - ) + ).limit(100) found = False for event_data in not_processed_events: From 318237ded65c42e04a61cc38ba91886c0becf7a4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 25 May 2023 16:38:01 +0200 Subject: [PATCH 071/147] breaking get_current_timeline into more functions --- openpype/hosts/resolve/api/__init__.py | 4 ++ openpype/hosts/resolve/api/lib.py | 83 ++++++++++++++++---------- openpype/hosts/resolve/api/plugin.py | 5 +- 3 files changed, 59 insertions(+), 33 deletions(-) diff --git a/openpype/hosts/resolve/api/__init__.py b/openpype/hosts/resolve/api/__init__.py index 00a598548e..2b4546f8d6 100644 --- a/openpype/hosts/resolve/api/__init__.py +++ b/openpype/hosts/resolve/api/__init__.py @@ -24,6 +24,8 @@ from .lib import ( get_project_manager, get_current_project, get_current_timeline, + get_any_timeline, + get_new_timeline, create_bin, get_media_pool_item, create_media_pool_item, @@ -95,6 +97,8 @@ __all__ = [ "get_project_manager", "get_current_project", "get_current_timeline", + "get_any_timeline", + "get_new_timeline", "create_bin", "get_media_pool_item", "create_media_pool_item", diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index d42521200a..a44c527f13 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -15,6 +15,7 @@ log = Logger.get_logger(__name__) self = sys.modules[__name__] self.project_manager = None self.media_storage = None +self.current_project = None # OpenPype sequential rename variables self.rename_index = 0 @@ -85,47 +86,60 @@ def get_media_storage(): def get_current_project(): - # initialize project manager - get_project_manager() + """Get current project object. + """ + if not self.current_project: + self.current_project = get_project_manager().GetCurrentProject() - return self.project_manager.GetCurrentProject() + return self.current_project -def get_current_timeline(new=False, get_any=False): +def get_current_timeline(new=False): """Get current timeline object. Args: - new (bool, optional): return only new timeline. Defaults to False. - get_any (bool, optional): return any even new if no timeline available. - Defaults to False. + new (bool)[optional]: [DEPRECATED] if True it will create + new timeline if none exists + + Returns: + TODO: will need to reflect future `None` + object: resolve.Timeline + """ + project = get_current_project() + timeline = project.GetCurrentTimeline() + + # return current timeline if any + if timeline: + return timeline + + # TODO: [deprecated] and will be removed in future + if new: + return get_new_timeline() + + +def get_any_timeline(): + """Get any timeline object. + + Returns: + object | None: resolve.Timeline + """ + project = get_current_project() + timeline_count = project.GetTimelineCount() + if timeline_count > 0: + return project.GetTimelineByIndex(1) + + +def get_new_timeline(): + """Get new timeline object. Returns: object: resolve.Timeline """ - # get current project project = get_current_project() - - timeline = project.GetCurrentTimeline() - - # return current timeline only if it is not new - if timeline and not new: - return timeline - - # if get_any is True then return any timeline - if get_any: - timeline_count = project.GetTimelineCount() - if timeline_count == 0: - # if there is no timeline then create a new one - new = True - else: - return project.GetTimelineByIndex(1) - - # create new timeline if new is True - if new: - media_pool = project.GetMediaPool() - new_timeline = media_pool.CreateEmptyTimeline(self.pype_timeline_name) - project.SetCurrentTimeline(new_timeline) - return new_timeline + media_pool = project.GetMediaPool() + new_timeline = media_pool.CreateEmptyTimeline(self.pype_timeline_name) + project.SetCurrentTimeline(new_timeline) + return new_timeline def create_bin(name: str, root: object = None) -> object: @@ -337,8 +351,13 @@ def get_current_timeline_items( track_type = track_type or "video" selecting_color = selecting_color or "Chocolate" project = get_current_project() - # make sure some timeline will be active with `any` argument - timeline = get_current_timeline(get_any=True) + + # get timeline anyhow + timeline = ( + get_current_timeline() or + get_any_timeline() or + get_new_timeline() + ) selected_clips = [] # get all tracks count filtered by track type diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index 609cff60f7..e5846c2fc2 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -327,7 +327,10 @@ class ClipLoader: self.active_timeline = options["timeline"] else: # create new sequence - self.active_timeline = lib.get_current_timeline(new=True) + self.active_timeline = ( + lib.get_current_timeline() or + lib.get_new_timeline() + ) else: self.active_timeline = lib.get_current_timeline() From c61dd1b24775c6438e3ba5844f5159ef1349b66a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 25 May 2023 17:18:23 +0200 Subject: [PATCH 072/147] utility scripts cosmetics only copy test and develop scripts if developer --- .../{__OpenPype__Menu__.py => OpenPype__Menu.py} | 0 openpype/hosts/resolve/utility_scripts/README.markdown | 1 - .../resolve/utility_scripts/{ => develop}/OTIO_export.py | 0 .../resolve/utility_scripts/{ => develop}/OTIO_import.py | 0 .../{ => develop}/OpenPype_sync_util_scripts.py | 0 openpype/hosts/resolve/utils.py | 9 ++++++++- 6 files changed, 8 insertions(+), 2 deletions(-) rename openpype/hosts/resolve/utility_scripts/{__OpenPype__Menu__.py => OpenPype__Menu.py} (100%) delete mode 100644 openpype/hosts/resolve/utility_scripts/README.markdown rename openpype/hosts/resolve/utility_scripts/{ => develop}/OTIO_export.py (100%) rename openpype/hosts/resolve/utility_scripts/{ => develop}/OTIO_import.py (100%) rename openpype/hosts/resolve/utility_scripts/{ => develop}/OpenPype_sync_util_scripts.py (100%) diff --git a/openpype/hosts/resolve/utility_scripts/__OpenPype__Menu__.py b/openpype/hosts/resolve/utility_scripts/OpenPype__Menu.py similarity index 100% rename from openpype/hosts/resolve/utility_scripts/__OpenPype__Menu__.py rename to openpype/hosts/resolve/utility_scripts/OpenPype__Menu.py diff --git a/openpype/hosts/resolve/utility_scripts/README.markdown b/openpype/hosts/resolve/utility_scripts/README.markdown deleted file mode 100644 index 8b13789179..0000000000 --- a/openpype/hosts/resolve/utility_scripts/README.markdown +++ /dev/null @@ -1 +0,0 @@ - diff --git a/openpype/hosts/resolve/utility_scripts/OTIO_export.py b/openpype/hosts/resolve/utility_scripts/develop/OTIO_export.py similarity index 100% rename from openpype/hosts/resolve/utility_scripts/OTIO_export.py rename to openpype/hosts/resolve/utility_scripts/develop/OTIO_export.py diff --git a/openpype/hosts/resolve/utility_scripts/OTIO_import.py b/openpype/hosts/resolve/utility_scripts/develop/OTIO_import.py similarity index 100% rename from openpype/hosts/resolve/utility_scripts/OTIO_import.py rename to openpype/hosts/resolve/utility_scripts/develop/OTIO_import.py diff --git a/openpype/hosts/resolve/utility_scripts/OpenPype_sync_util_scripts.py b/openpype/hosts/resolve/utility_scripts/develop/OpenPype_sync_util_scripts.py similarity index 100% rename from openpype/hosts/resolve/utility_scripts/OpenPype_sync_util_scripts.py rename to openpype/hosts/resolve/utility_scripts/develop/OpenPype_sync_util_scripts.py diff --git a/openpype/hosts/resolve/utils.py b/openpype/hosts/resolve/utils.py index 8e5dd9a188..9a161f4865 100644 --- a/openpype/hosts/resolve/utils.py +++ b/openpype/hosts/resolve/utils.py @@ -1,6 +1,6 @@ import os import shutil -from openpype.lib import Logger +from openpype.lib import Logger, is_running_from_build RESOLVE_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -41,6 +41,13 @@ def setup(env): # copy scripts into Resolve's utility scripts dir for directory, scripts in scripts.items(): for script in scripts: + if ( + is_running_from_build() and + script in ["tests", "develop"] + ): + # only copy those if started from build + continue + src = os.path.join(directory, script) dst = os.path.join(util_scripts_dir, script) log.info("Copying `{}` to `{}`...".format(src, dst)) From b47143b472eec6bb35ced0bbbb1d2b9a77c9acd4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 25 May 2023 17:41:36 +0200 Subject: [PATCH 073/147] collect frames to fix settings --- .../plugins/publish/collect_frames_fix.py | 21 ++++++++++++++----- .../defaults/project_settings/global.json | 8 ++++++- .../schemas/schema_global_publish.json | 20 ++++++++++++++++++ 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/openpype/plugins/publish/collect_frames_fix.py b/openpype/plugins/publish/collect_frames_fix.py index bdd49585a5..837738eb06 100644 --- a/openpype/plugins/publish/collect_frames_fix.py +++ b/openpype/plugins/publish/collect_frames_fix.py @@ -26,11 +26,13 @@ class CollectFramesFixDef( targets = ["local"] hosts = ["nuke"] families = ["render", "prerender"] - enabled = True + + rewrite_version_enable = False def process(self, instance): attribute_values = self.get_attr_values_from_data(instance.data) frames_to_fix = attribute_values.get("frames_to_fix") + rewrite_version = attribute_values.get("rewrite_version") if frames_to_fix: @@ -71,10 +73,19 @@ class CollectFramesFixDef( @classmethod def get_attribute_defs(cls): - return [ + attributes = [ TextDef("frames_to_fix", label="Frames to fix", placeholder="5,10-15", - regex="[0-9,-]+"), - BoolDef("rewrite_version", label="Rewrite latest version", - default=False), + regex="[0-9,-]+") ] + + if cls.rewrite_version_enable: + attributes.append( + BoolDef( + "rewrite_version", + label="Rewrite latest version", + default=False + ) + ) + + return attributes diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 75f335f1de..002e547feb 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -46,6 +46,10 @@ "enabled": false, "families": [] }, + "CollectFramesFixDef": { + "enabled": true, + "rewrite_version_enable": true + }, "ValidateEditorialAssetName": { "enabled": true, "optional": false @@ -252,7 +256,9 @@ } }, { - "families": ["review"], + "families": [ + "review" + ], "hosts": [ "maya", "houdini" diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index a7617918a3..8000e6156b 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -81,6 +81,26 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "checkbox_key": "enabled", + "key": "CollectFramesFixDef", + "label": "Collect Frames to Fix", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "rewrite_version_enable", + "label": "Rewrite latest version" + } + ] + }, { "type": "dict", "collapsible": true, From 31da5582fca78f9f84977700c69523e077df6e19 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 25 May 2023 21:39:07 +0200 Subject: [PATCH 074/147] make understandable label in settings --- .../schemas/projects_schema/schemas/schema_global_publish.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 8000e6156b..3164cfb62d 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -97,7 +97,7 @@ { "type": "boolean", "key": "rewrite_version_enable", - "label": "Rewrite latest version" + "label": "Show 'Rewrite latest version' toggle" } ] }, From 682d8e6b0551935645724e0b632fb5736448979f Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 26 May 2023 12:28:45 +0800 Subject: [PATCH 075/147] custom script for setting frame range for read node --- openpype/hosts/nuke/api/lib.py | 30 ++++++ openpype/hosts/nuke/api/pipeline.py | 5 + openpype/widgets/custom_popup.py | 141 ++++++++++++++++++++++++++++ 3 files changed, 176 insertions(+) create mode 100644 openpype/widgets/custom_popup.py diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index a439142051..1aa0a95c86 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2358,6 +2358,36 @@ class WorkfileSettings(object): # add colorspace menu item self.set_colorspace() + def reset_frame_range_read_nodes(self): + from openpype.widgets import custom_popup + parent = get_main_window() + dialog = custom_popup.CustomScriptDialog(parent=parent) + dialog.setWindowTitle("Frame Range for Read Node") + dialog.set_name("Frame Range: ") + dialog.set_line_edit("%s - %s" % (nuke.root().firstFrame(), + nuke.root().lastFrame())) + frame = dialog.widgets["line_edit"] + selection = dialog.widgets["selection"] + dialog.on_clicked.connect( + lambda: set_frame_range(frame, selection) + ) + def set_frame_range(frame, selection): + frame_range = frame.text() + selected = selection.isChecked() + for read_node in nuke.allNodes("Read"): + if selected: + if not nuke.selectedNodes(): + return + if read_node in nuke.selectedNodes(): + read_node["frame_mode"].setValue("start_at") + read_node["frame"].setValue(frame_range) + else: + read_node["frame_mode"].setValue("start_at") + read_node["frame"].setValue(frame_range) + dialog.show() + + return False + def set_favorites(self): from .utils import set_context_favorites diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index d649ffae7f..8d6be76e48 100644 --- a/openpype/hosts/nuke/api/pipeline.py +++ b/openpype/hosts/nuke/api/pipeline.py @@ -282,6 +282,11 @@ def _install_menu(): lambda: WorkfileSettings().set_context_settings() ) + menu.addSeparator() + menu.addCommand( + "Set Frame Range(Read Node)", + lambda: WorkfileSettings().reset_frame_range_read_nodes() + ) menu.addSeparator() menu.addCommand( "Build Workfile", diff --git a/openpype/widgets/custom_popup.py b/openpype/widgets/custom_popup.py new file mode 100644 index 0000000000..c4bb1ad43b --- /dev/null +++ b/openpype/widgets/custom_popup.py @@ -0,0 +1,141 @@ +import sys +import contextlib + +from PySide2 import QtCore, QtWidgets + + +class CustomScriptDialog(QtWidgets.QDialog): + """A Popup that moves itself to bottom right of screen on show event. + + The UI contains a message label and a red highlighted button to "show" + or perform another custom action from this pop-up. + + """ + + on_clicked = QtCore.Signal() + on_line_changed = QtCore.Signal(str) + + def __init__(self, parent=None, *args, **kwargs): + super(CustomScriptDialog, self).__init__(parent=parent, *args, **kwargs) + self.setContentsMargins(0, 0, 0, 0) + + # Layout + layout = QtWidgets.QVBoxLayout(self) + line_layout = QtWidgets.QHBoxLayout() + line_layout.setContentsMargins(10, 5, 10, 10) + selection_layout = QtWidgets.QHBoxLayout() + selection_layout.setContentsMargins(10, 5, 10, 10) + button_layout = QtWidgets.QHBoxLayout() + button_layout.setContentsMargins(10, 5, 10, 10) + + # Increase spacing slightly for readability + line_layout.setSpacing(10) + button_layout.setSpacing(8) + name = QtWidgets.QLabel("") + name.setStyleSheet(""" + QLabel { + font-size: 12px; + } + """) + line_edit = QtWidgets.QLineEdit("") + selection_name = QtWidgets.QLabel("Use Selection") + selection_name.setStyleSheet(""" + QLabel { + font-size: 12px; + } + """) + has_selection = QtWidgets.QCheckBox() + button = QtWidgets.QPushButton("Execute") + button.setSizePolicy(QtWidgets.QSizePolicy.Maximum, + QtWidgets.QSizePolicy.Maximum) + cancel = QtWidgets.QPushButton("Cancel") + cancel.setSizePolicy(QtWidgets.QSizePolicy.Maximum, + QtWidgets.QSizePolicy.Maximum) + + line_layout.addWidget(name) + line_layout.addWidget(line_edit) + selection_layout.addWidget(selection_name) + selection_layout.addWidget(has_selection) + button_layout.addWidget(button) + button_layout.addWidget(cancel) + layout.addLayout(line_layout) + layout.addLayout(selection_layout) + layout.addLayout(button_layout) + # Default size + self.resize(100, 30) + + self.widgets = { + "name": name, + "line_edit": line_edit, + "selection": has_selection, + "button": button, + "cancel": cancel + } + + # Signals + has_selection.toggled.connect(self.emit_click_with_state) + line_edit.textChanged.connect(self.on_line_edit_changed) + button.clicked.connect(self._on_clicked) + cancel.clicked.connect(self.close) + self.update_values() + # Set default title + self.setWindowTitle("Custom Popup") + + def update_values(self): + self.widgets["selection"].isChecked() + + def emit_click_with_state(self): + """Emit the on_clicked signal with the toggled state""" + checked = self.widgets["selection"].isChecked() + return checked + + def set_name(self, name): + self.widgets['name'].setText(name) + + def set_line_edit(self, line_edit): + self.widgets['line_edit'].setText(line_edit) + print(line_edit) + + def setButtonText(self, text): + self.widgets["button"].setText(text) + + def setCancelText(self, text): + self.widgets["cancel"].setText(text) + + def on_line_edit_changed(self): + line_edit = self.widgets['line_edit'].text() + self.on_line_changed.emit(line_edit) + return self.set_line_edit(line_edit) + + + def _on_clicked(self): + """Callback for when the 'show' button is clicked. + + Raises the parent (if any) + + """ + + parent = self.parent() + self.close() + + # Trigger the signal + self.on_clicked.emit() + + if parent: + parent.raise_() + + def showEvent(self, event): + + # Position popup based on contents on show event + return super(CustomScriptDialog, self).showEvent(event) + +@contextlib.contextmanager +def application(): + app = QtWidgets.QApplication(sys.argv) + yield + app.exec_() + +if __name__ == "__main__": + with application(): + dialog = CustomScriptDialog() + dialog.show() From 3ece2a8fcf73fa7d0da438ef056447fd7967c6ee Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 26 May 2023 12:56:11 +0800 Subject: [PATCH 076/147] clean up & cosmetic fix --- openpype/hosts/nuke/api/lib.py | 5 ++++- openpype/widgets/custom_popup.py | 12 +++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 1aa0a95c86..94a0ff15ad 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2362,7 +2362,7 @@ class WorkfileSettings(object): from openpype.widgets import custom_popup parent = get_main_window() dialog = custom_popup.CustomScriptDialog(parent=parent) - dialog.setWindowTitle("Frame Range for Read Node") + dialog.setWindowTitle("Frame Range") dialog.set_name("Frame Range: ") dialog.set_line_edit("%s - %s" % (nuke.root().firstFrame(), nuke.root().lastFrame())) @@ -2371,9 +2371,12 @@ class WorkfileSettings(object): dialog.on_clicked.connect( lambda: set_frame_range(frame, selection) ) + def set_frame_range(frame, selection): frame_range = frame.text() selected = selection.isChecked() + if not nuke.allNodes("Read"): + return for read_node in nuke.allNodes("Read"): if selected: if not nuke.selectedNodes(): diff --git a/openpype/widgets/custom_popup.py b/openpype/widgets/custom_popup.py index c4bb1ad43b..85e31d6ce0 100644 --- a/openpype/widgets/custom_popup.py +++ b/openpype/widgets/custom_popup.py @@ -16,7 +16,9 @@ class CustomScriptDialog(QtWidgets.QDialog): on_line_changed = QtCore.Signal(str) def __init__(self, parent=None, *args, **kwargs): - super(CustomScriptDialog, self).__init__(parent=parent, *args, **kwargs) + super(CustomScriptDialog, self).__init__(parent=parent, + *args, + **kwargs) self.setContentsMargins(0, 0, 0, 0) # Layout @@ -30,7 +32,7 @@ class CustomScriptDialog(QtWidgets.QDialog): # Increase spacing slightly for readability line_layout.setSpacing(10) - button_layout.setSpacing(8) + button_layout.setSpacing(10) name = QtWidgets.QLabel("") name.setStyleSheet(""" QLabel { @@ -47,7 +49,7 @@ class CustomScriptDialog(QtWidgets.QDialog): has_selection = QtWidgets.QCheckBox() button = QtWidgets.QPushButton("Execute") button.setSizePolicy(QtWidgets.QSizePolicy.Maximum, - QtWidgets.QSizePolicy.Maximum) + QtWidgets.QSizePolicy.Maximum) cancel = QtWidgets.QPushButton("Cancel") cancel.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum) @@ -62,7 +64,7 @@ class CustomScriptDialog(QtWidgets.QDialog): layout.addLayout(selection_layout) layout.addLayout(button_layout) # Default size - self.resize(100, 30) + self.resize(100, 40) self.widgets = { "name": name, @@ -107,7 +109,6 @@ class CustomScriptDialog(QtWidgets.QDialog): self.on_line_changed.emit(line_edit) return self.set_line_edit(line_edit) - def _on_clicked(self): """Callback for when the 'show' button is clicked. @@ -129,6 +130,7 @@ class CustomScriptDialog(QtWidgets.QDialog): # Position popup based on contents on show event return super(CustomScriptDialog, self).showEvent(event) + @contextlib.contextmanager def application(): app = QtWidgets.QApplication(sys.argv) From 54cccc6a6af7f4d3e04e47173b3e302dbc5d2aa0 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 26 May 2023 12:57:05 +0800 Subject: [PATCH 077/147] hound --- openpype/widgets/custom_popup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/widgets/custom_popup.py b/openpype/widgets/custom_popup.py index 85e31d6ce0..be4b0c32d5 100644 --- a/openpype/widgets/custom_popup.py +++ b/openpype/widgets/custom_popup.py @@ -137,6 +137,7 @@ def application(): yield app.exec_() + if __name__ == "__main__": with application(): dialog = CustomScriptDialog() From 7e02416d30e4d33282a37b27331272701f276d17 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 26 May 2023 17:35:13 +0800 Subject: [PATCH 078/147] hound fix --- openpype/hosts/max/api/lib_renderproducts.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 1073a0e19e..19c1048496 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -150,7 +150,7 @@ class RenderProducts(object): def get_arnold_product_name(self, folder, startFrame, endFrame, fmt): """Get all the Arnold AOVs""" - aovs + aov_dict = {} amw = rt.MaxtoAOps.AOVsManagerWindow() aov_mgr = rt.renderers.current.AOVManager @@ -187,7 +187,7 @@ class RenderProducts(object): render_element = render_element.replace("\\", "/") render_dict.update({renderpass: render_element}) - return render_dirname + return render_dict def image_format(self): return self._project_settings["max"]["RenderSettings"]["image_format"] # noqa From 9f7f22961b6b30c256c7a60d8f16ea18058e1a62 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 26 May 2023 10:43:54 +0100 Subject: [PATCH 079/147] Improved implementation of UMap to use UAsset base code --- .../unreal/plugins/create/create_uasset.py | 24 ++- .../unreal/plugins/create/create_umap.py | 46 ------ .../hosts/unreal/plugins/load/load_uasset.py | 28 ++-- .../hosts/unreal/plugins/load/load_umap.py | 140 ------------------ .../unreal/plugins/publish/extract_uasset.py | 15 +- .../unreal/plugins/publish/extract_umap.py | 48 ------ 6 files changed, 49 insertions(+), 252 deletions(-) delete mode 100644 openpype/hosts/unreal/plugins/create/create_umap.py delete mode 100644 openpype/hosts/unreal/plugins/load/load_umap.py delete mode 100644 openpype/hosts/unreal/plugins/publish/extract_umap.py diff --git a/openpype/hosts/unreal/plugins/create/create_uasset.py b/openpype/hosts/unreal/plugins/create/create_uasset.py index c78518e86b..f70ecc55b3 100644 --- a/openpype/hosts/unreal/plugins/create/create_uasset.py +++ b/openpype/hosts/unreal/plugins/create/create_uasset.py @@ -17,6 +17,8 @@ class CreateUAsset(UnrealAssetCreator): family = "uasset" icon = "cube" + extension = ".uasset" + def create(self, subset_name, instance_data, pre_create_data): if pre_create_data.get("use_selection"): ar = unreal.AssetRegistryHelpers.get_asset_registry() @@ -37,10 +39,28 @@ class CreateUAsset(UnrealAssetCreator): f"{Path(obj).name} is not on the disk. Likely it needs to" "be saved first.") - if Path(sys_path).suffix != ".uasset": - raise CreatorError(f"{Path(sys_path).name} is not a UAsset.") + if Path(sys_path).suffix != self.extension: + raise CreatorError( + f"{Path(sys_path).name} is not a {self.label}.") super(CreateUAsset, self).create( subset_name, instance_data, pre_create_data) + + +class CreateUMap(CreateUAsset): + """Create Level.""" + + identifier = "io.ayon.creators.unreal.umap" + label = "Level" + family = "uasset" + extension = ".umap" + + def create(self, subset_name, instance_data, pre_create_data): + instance_data["families"] = ["umap"] + + super(CreateUMap, self).create( + subset_name, + instance_data, + pre_create_data) diff --git a/openpype/hosts/unreal/plugins/create/create_umap.py b/openpype/hosts/unreal/plugins/create/create_umap.py deleted file mode 100644 index 34aa8cdc00..0000000000 --- a/openpype/hosts/unreal/plugins/create/create_umap.py +++ /dev/null @@ -1,46 +0,0 @@ -# -*- coding: utf-8 -*- -from pathlib import Path - -import unreal - -from openpype.pipeline import CreatorError -from openpype.hosts.unreal.api.plugin import ( - UnrealAssetCreator, -) - - -class CreateUMap(UnrealAssetCreator): - """Create Level.""" - - identifier = "io.ayon.creators.unreal.umap" - label = "Level" - family = "uasset" - icon = "cube" - - def create(self, subset_name, instance_data, pre_create_data): - if pre_create_data.get("use_selection"): - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() - selection = [a.get_path_name() for a in sel_objects] - - if len(selection) != 1: - raise CreatorError("Please select only one object.") - - obj = selection[0] - - asset = ar.get_asset_by_object_path(obj).get_asset() - sys_path = unreal.SystemLibrary.get_system_path(asset) - - if not sys_path: - raise CreatorError( - f"{Path(obj).name} is not on the disk. Likely it needs to" - "be saved first.") - - if Path(sys_path).suffix != ".umap": - raise CreatorError(f"{Path(sys_path).name} is not a Level.") - - super(CreateUMap, self).create( - subset_name, - instance_data, - pre_create_data) diff --git a/openpype/hosts/unreal/plugins/load/load_uasset.py b/openpype/hosts/unreal/plugins/load/load_uasset.py index 7606bc14e4..44c87593e9 100644 --- a/openpype/hosts/unreal/plugins/load/load_uasset.py +++ b/openpype/hosts/unreal/plugins/load/load_uasset.py @@ -21,6 +21,8 @@ class UAssetLoader(plugin.Loader): icon = "cube" color = "orange" + extension = "uasset" + def load(self, context, name, namespace, options): """Load and containerise representation into Content Browser. @@ -42,11 +44,7 @@ class UAssetLoader(plugin.Loader): root = "/Game/Ayon/Assets" asset = context.get('asset').get('name') suffix = "_CON" - if asset: - asset_name = "{}_{}".format(asset, name) - else: - asset_name = "{}".format(name) - + asset_name = f"{asset}_{name}" if asset else f"{name}" tools = unreal.AssetToolsHelpers().get_asset_tools() asset_dir, container_name = tools.create_unique_asset_name( f"{root}/{asset}/{name}", suffix="" @@ -61,7 +59,7 @@ class UAssetLoader(plugin.Loader): Path(unreal.Paths.project_content_dir()).as_posix(), 1) - shutil.copy(self.fname, f"{destination_path}/{name}.uasset") + shutil.copy(self.fname, f"{destination_path}/{name}.{self.extension}") # Create Asset Container unreal_pipeline.create_container( @@ -107,15 +105,15 @@ class UAssetLoader(plugin.Loader): for asset in asset_content: obj = ar.get_asset_by_object_path(asset).get_asset() - if not obj.get_class().get_name() == 'AyonAssetContainer': + if obj.get_class().get_name() != 'AyonAssetContainer': unreal.EditorAssetLibrary.delete_asset(asset) update_filepath = get_representation_path(representation) - shutil.copy(update_filepath, f"{destination_path}/{name}.uasset") + shutil.copy( + update_filepath, f"{destination_path}/{name}.{self.extension}") - container_path = "{}/{}".format(container["namespace"], - container["objectName"]) + container_path = f'{container["namespace"]}/{container["objectName"]}' # update metadata unreal_pipeline.imprint( container_path, @@ -143,3 +141,13 @@ class UAssetLoader(plugin.Loader): if len(asset_content) == 0: unreal.EditorAssetLibrary.delete_directory(parent_path) + + +class UMapLoader(UAssetLoader): + """Load Level.""" + + families = ["uasset"] + label = "Load Level" + representations = ["umap"] + + extension = "umap" diff --git a/openpype/hosts/unreal/plugins/load/load_umap.py b/openpype/hosts/unreal/plugins/load/load_umap.py deleted file mode 100644 index f467fe6b3b..0000000000 --- a/openpype/hosts/unreal/plugins/load/load_umap.py +++ /dev/null @@ -1,140 +0,0 @@ -# -*- coding: utf-8 -*- -"""Load Level.""" -from pathlib import Path -import shutil - -from openpype.pipeline import ( - get_representation_path, - AYON_CONTAINER_ID -) -from openpype.hosts.unreal.api import plugin -from openpype.hosts.unreal.api import pipeline as unreal_pipeline -import unreal # noqa - - -class UMapLoader(plugin.Loader): - """Load Level.""" - - families = ["uasset"] - label = "Load Level" - representations = ["umap"] - icon = "cube" - color = "orange" - - def load(self, context, name, namespace, options): - """Load and containerise representation into Content Browser. - - Args: - context (dict): application context - name (str): subset name - namespace (str): in Unreal this is basically path to container. - This is not passed here, so namespace is set - by `containerise()` because only then we know - real path. - options (dict): Those would be data to be imprinted. This is not - used now, data are imprinted by `containerise()`. - - Returns: - list(str): list of container content - """ - - # Create directory for asset and Ayon container - root = "/Game/Ayon/Assets" - asset = context.get('asset').get('name') - suffix = "_CON" - asset_name = f"{asset}_{name}" if asset else f"{name}" - tools = unreal.AssetToolsHelpers().get_asset_tools() - asset_dir, container_name = tools.create_unique_asset_name( - f"{root}/{asset}/{name}", suffix="" - ) - - container_name += suffix - - unreal.EditorAssetLibrary.make_directory(asset_dir) - - destination_path = asset_dir.replace( - "/Game", - Path(unreal.Paths.project_content_dir()).as_posix(), - 1) - - shutil.copy(self.fname, f"{destination_path}/{name}.uasset") - - # Create Asset Container - unreal_pipeline.create_container( - container=container_name, path=asset_dir) - - data = { - "schema": "ayon:container-2.0", - "id": AYON_CONTAINER_ID, - "asset": asset, - "namespace": asset_dir, - "container_name": container_name, - "asset_name": asset_name, - "loader": str(self.__class__.__name__), - "representation": context["representation"]["_id"], - "parent": context["representation"]["parent"], - "family": context["representation"]["context"]["family"] - } - unreal_pipeline.imprint(f"{asset_dir}/{container_name}", data) - - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=True - ) - - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) - - return asset_content - - def update(self, container, representation): - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - asset_dir = container["namespace"] - name = representation["context"]["subset"] - - destination_path = asset_dir.replace( - "/Game", - Path(unreal.Paths.project_content_dir()).as_posix(), - 1) - - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=False, include_folder=True - ) - - for asset in asset_content: - obj = ar.get_asset_by_object_path(asset).get_asset() - if obj.get_class().get_name() != 'AyonAssetContainer': - unreal.EditorAssetLibrary.delete_asset(asset) - - update_filepath = get_representation_path(representation) - - shutil.copy(update_filepath, f"{destination_path}/{name}.umap") - - container_path = f'{container["namespace"]}/{container["objectName"]}' - # update metadata - unreal_pipeline.imprint( - container_path, - { - "representation": str(representation["_id"]), - "parent": str(representation["parent"]) - }) - - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=True - ) - - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) - - def remove(self, container): - path = container["namespace"] - parent_path = Path(path).parent.as_posix() - - unreal.EditorAssetLibrary.delete_directory(path) - - asset_content = unreal.EditorAssetLibrary.list_assets( - parent_path, recursive=False - ) - - if len(asset_content) == 0: - unreal.EditorAssetLibrary.delete_directory(parent_path) diff --git a/openpype/hosts/unreal/plugins/publish/extract_uasset.py b/openpype/hosts/unreal/plugins/publish/extract_uasset.py index f719df2a82..48b62faa97 100644 --- a/openpype/hosts/unreal/plugins/publish/extract_uasset.py +++ b/openpype/hosts/unreal/plugins/publish/extract_uasset.py @@ -11,16 +11,17 @@ class ExtractUAsset(publish.Extractor): label = "Extract UAsset" hosts = ["unreal"] - families = ["uasset"] + families = ["uasset", "umap"] optional = True def process(self, instance): + extension = ( + "umap" if "umap" in instance.data.get("families") else "uasset") ar = unreal.AssetRegistryHelpers.get_asset_registry() self.log.info("Performing extraction..") - staging_dir = self.staging_dir(instance) - filename = "{}.uasset".format(instance.name) + filename = f"{instance.name}.{extension}" members = instance.data.get("members", []) @@ -36,13 +37,15 @@ class ExtractUAsset(publish.Extractor): shutil.copy(sys_path, staging_dir) + self.log.info(f"instance.data: {instance.data}") + if "representations" not in instance.data: instance.data["representations"] = [] representation = { - 'name': 'uasset', - 'ext': 'uasset', - 'files': filename, + "name": extension, + "ext": extension, + "files": filename, "stagingDir": staging_dir, } instance.data["representations"].append(representation) diff --git a/openpype/hosts/unreal/plugins/publish/extract_umap.py b/openpype/hosts/unreal/plugins/publish/extract_umap.py deleted file mode 100644 index 3812834430..0000000000 --- a/openpype/hosts/unreal/plugins/publish/extract_umap.py +++ /dev/null @@ -1,48 +0,0 @@ -from pathlib import Path -import shutil - -import unreal - -from openpype.pipeline import publish - - -class ExtractUMap(publish.Extractor): - """Extract a UMap.""" - - label = "Extract Level" - hosts = ["unreal"] - families = ["uasset"] - optional = True - - def process(self, instance): - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - self.log.info("Performing extraction..") - - staging_dir = self.staging_dir(instance) - filename = f"{instance.name}.umap" - - members = instance.data.get("members", []) - - if not members: - raise RuntimeError("No members found in instance.") - - # UAsset publishing supports only one member - obj = members[0] - - asset = ar.get_asset_by_object_path(obj).get_asset() - sys_path = unreal.SystemLibrary.get_system_path(asset) - filename = Path(sys_path).name - - shutil.copy(sys_path, staging_dir) - - if "representations" not in instance.data: - instance.data["representations"] = [] - - representation = { - 'name': 'umap', - 'ext': 'umap', - 'files': filename, - "stagingDir": staging_dir, - } - instance.data["representations"].append(representation) From 5f98c278361d9354565261174e51b43b5acaffa9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 26 May 2023 12:00:09 +0200 Subject: [PATCH 080/147] apply settings on publish plugins can expect only project settings (#5037) --- openpype/pipeline/publish/lib.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index e87b865dce..f228709b3b 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -12,7 +12,8 @@ import pyblish.api from openpype.lib import ( Logger, import_filepath, - filter_profiles + filter_profiles, + is_func_signature_supported, ) from openpype.settings import ( get_project_settings, @@ -498,12 +499,26 @@ def filter_pyblish_plugins(plugins): # iterate over plugins for plugin in plugins[:]: # Apply settings to plugins - if hasattr(plugin, "apply_settings"): + + apply_settings_func = getattr(plugin, "apply_settings", None) + if apply_settings_func is not None: # Use classmethod 'apply_settings' # - can be used to target settings from custom settings place # - skip default behavior when successful try: - plugin.apply_settings(project_settings, system_settings) + # Support to pass only project settings + # - make sure that both settings are passed, when can be + # - that covers cases when *args are in method parameters + both_supported = is_func_signature_supported( + apply_settings_func, project_settings, system_settings + ) + project_supported = is_func_signature_supported( + apply_settings_func, project_settings + ) + if not both_supported and project_supported: + plugin.apply_settings(project_settings) + else: + plugin.apply_settings(project_settings, system_settings) except Exception: log.warning( From e633cc7decbcfb8642f7d66ad17fe4219805e834 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 26 May 2023 18:19:50 +0800 Subject: [PATCH 081/147] expected file can get the aov path --- openpype/hosts/max/api/lib_renderproducts.py | 4 +++- .../max/plugins/publish/collect_render.py | 19 ++----------------- .../plugins/publish/submit_publish_job.py | 2 +- 3 files changed, 6 insertions(+), 19 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 19c1048496..ba1ffc3a5e 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -70,7 +70,7 @@ class RenderProducts(object): return rgba_render_list, render_elem_list - def get_aov(self): + def get_aovs(self): folder = rt.maxFilePath folder = folder.replace("\\", "/") setting = self._project_settings @@ -177,6 +177,8 @@ class RenderProducts(object): render_elem = rt.maxOps.GetCurRenderElementMgr() render_elem_num = render_elem.NumRenderElements() + if render_elem_num < 1: + return # get render elements from the renders for i in range(render_elem_num): renderlayer_name = render_elem.GetRenderElement(i) diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 652c2e1d2c..c4a44a5b11 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -45,23 +45,8 @@ class CollectRender(pyblish.api.InstancePlugin): } folder = folder.replace("\\", "/") - if aov_list: - if renderer in [ - "ART_Renderer", - "V_Ray_6_Hotfix_3", - "V_Ray_GPU_6_Hotfix_3" - "Redshift_Renderer", - "Default_Scanline_Renderer", - "Quicksilver_Hardware_Renderer", - ]: - - render_element = RenderProducts().get_aov() - files_by_aov.update(render_element) - self.log.debug(files_by_aov) - - if renderer == "Arnold": - aovs = RenderProducts().get_aovs() - files_by_aov.update(aovs) + aovs = RenderProducts().get_aovs() + files_by_aov.update(aovs) if "expectedFiles" not in instance.data: instance.data["expectedFiles"] = list() diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 7133cff058..68eb0a437d 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -348,7 +348,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): self.log.info("Submitting Deadline job ...") url = "{}/api/jobs".format(self.deadline_url) - response = requests.post(url, json=payload, timeout=10, verify=False) + response = requests.post(url, json=payload, timeout=10) if not response.ok: raise Exception(response.text) From 905c3dbd249bce6c7d229e10466b3035aa2a6d40 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 26 May 2023 12:01:36 +0100 Subject: [PATCH 082/147] Fix problem when trying to load the same level multiple times --- .../hosts/unreal/plugins/load/load_uasset.py | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_uasset.py b/openpype/hosts/unreal/plugins/load/load_uasset.py index 44c87593e9..30f63abe39 100644 --- a/openpype/hosts/unreal/plugins/load/load_uasset.py +++ b/openpype/hosts/unreal/plugins/load/load_uasset.py @@ -50,16 +50,23 @@ class UAssetLoader(plugin.Loader): f"{root}/{asset}/{name}", suffix="" ) - container_name += suffix + unique_number = 1 + while unreal.EditorAssetLibrary.does_directory_exist( + f"{asset_dir}_{unique_number:02}" + ): + unique_number += 1 + + asset_dir = f"{asset_dir}_{unique_number:02}" + container_name = f"{container_name}_{unique_number:02}{suffix}" unreal.EditorAssetLibrary.make_directory(asset_dir) destination_path = asset_dir.replace( - "/Game", - Path(unreal.Paths.project_content_dir()).as_posix(), - 1) + "/Game", Path(unreal.Paths.project_content_dir()).as_posix(), 1) - shutil.copy(self.fname, f"{destination_path}/{name}.{self.extension}") + shutil.copy( + self.fname, + f"{destination_path}/{name}_{unique_number:02}.{self.extension}") # Create Asset Container unreal_pipeline.create_container( @@ -75,7 +82,7 @@ class UAssetLoader(plugin.Loader): "loader": str(self.__class__.__name__), "representation": context["representation"]["_id"], "parent": context["representation"]["parent"], - "family": context["representation"]["context"]["family"] + "family": context["representation"]["context"]["family"], } unreal_pipeline.imprint(f"{asset_dir}/{container_name}", data) @@ -94,10 +101,10 @@ class UAssetLoader(plugin.Loader): asset_dir = container["namespace"] name = representation["context"]["subset"] + unique_number = container["container_name"].split("_")[-2] + destination_path = asset_dir.replace( - "/Game", - Path(unreal.Paths.project_content_dir()).as_posix(), - 1) + "/Game", Path(unreal.Paths.project_content_dir()).as_posix(), 1) asset_content = unreal.EditorAssetLibrary.list_assets( asset_dir, recursive=False, include_folder=True @@ -105,13 +112,14 @@ class UAssetLoader(plugin.Loader): for asset in asset_content: obj = ar.get_asset_by_object_path(asset).get_asset() - if obj.get_class().get_name() != 'AyonAssetContainer': + if obj.get_class().get_name() != "AyonAssetContainer": unreal.EditorAssetLibrary.delete_asset(asset) update_filepath = get_representation_path(representation) shutil.copy( - update_filepath, f"{destination_path}/{name}.{self.extension}") + update_filepath, + f"{destination_path}/{name}_{unique_number}.{self.extension}") container_path = f'{container["namespace"]}/{container["objectName"]}' # update metadata @@ -119,8 +127,9 @@ class UAssetLoader(plugin.Loader): container_path, { "representation": str(representation["_id"]), - "parent": str(representation["parent"]) - }) + "parent": str(representation["parent"]), + } + ) asset_content = unreal.EditorAssetLibrary.list_assets( asset_dir, recursive=True, include_folder=True From 2d3ba2af0576d5a201fafa0a0957e18d0aa9d00a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 26 May 2023 19:36:14 +0800 Subject: [PATCH 083/147] add _beauty to subset name --- openpype/hosts/max/api/lib_renderproducts.py | 82 +++++++++++++------ .../max/plugins/publish/collect_render.py | 8 +- .../plugins/publish/submit_publish_job.py | 2 +- 3 files changed, 62 insertions(+), 30 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index ba1ffc3a5e..a93a1d821d 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -70,16 +70,26 @@ class RenderProducts(object): return rgba_render_list, render_elem_list - def get_aovs(self): + def get_aovs(self, container): folder = rt.maxFilePath + file = rt.maxFileName folder = folder.replace("\\", "/") setting = self._project_settings + render_folder = get_default_render_folder(setting) + filename, ext = os.path.splitext(file) + + output_file = os.path.join(folder, + render_folder, + filename, + container) + setting = self._project_settings img_fmt = setting["max"]["RenderSettings"]["image_format"] # noqa startFrame = int(rt.rendStart) endFrame = int(rt.rendEnd) + 1 renderer_class = get_current_renderer() renderer = str(renderer_class).split(":")[0] + render_dict = {} if renderer in [ "ART_Renderer", "Redshift_Renderer", @@ -88,12 +98,22 @@ class RenderProducts(object): "Default_Scanline_Renderer", "Quicksilver_Hardware_Renderer", ]: - render_dict = self.get_render_elements_name( - folder, startFrame, endFrame, img_fmt) + render_name = self.get_render_elements_name() + if render_name: + for name in render_name: + render_dict.update({ + name: self.get_expected_render_elements( + output_file, name, startFrame, endFrame, img_fmt) + }) if renderer == "Arnold": - render_dict = self.get_arnold_product_name( - folder, startFrame, endFrame, img_fmt) + render_name = self.get_arnold_product_name() + if render_name: + for name in render_name: + render_dict.update({ + name: self.get_expected_arnold_product( + output_file, name, startFrame, endFrame, img_fmt) + }) return render_dict @@ -148,9 +168,9 @@ class RenderProducts(object): return render_dirname - def get_arnold_product_name(self, folder, startFrame, endFrame, fmt): - """Get all the Arnold AOVs""" - aov_dict = {} + def get_arnold_product_name(self): + """Get all the Arnold AOVs name""" + aov_name = [] amw = rt.MaxtoAOps.AOVsManagerWindow() aov_mgr = rt.renderers.current.AOVManager @@ -161,20 +181,27 @@ class RenderProducts(object): for i in range(aov_group_num): # get the specific AOV group for aov in aov_mgr.drivers[i].aov_list: - for f in range(startFrame, endFrame): - render_element = f"{folder}_{aov.name}.{f}.{fmt}" - render_element = render_element.replace("\\", "/") - aov = str(aov.name) - aov_dict.update({aov: render_element}) + aov_name.append(aov.name) + # close the AOVs manager window amw.close() - return aov_dict + return aov_name - def get_render_elements_name(self, folder, startFrame, endFrame, fmt): - """Get all the render element output files. """ - render_dict = {} + def get_expected_arnold_product(self, folder, name, + startFrame, endFrame, fmt): + """Get all the expected Arnold AOVs""" + aov_list = [] + for f in range(startFrame, endFrame): + render_element = f"{folder}_{name}.{f}.{fmt}" + render_element = render_element.replace("\\", "/") + aov_list.append(render_element) + return aov_list + + def get_render_elements_name(self): + """Get all the render element names. """ + render_name = [] render_elem = rt.maxOps.GetCurRenderElementMgr() render_elem_num = render_elem.NumRenderElements() if render_elem_num < 1: @@ -182,14 +209,21 @@ class RenderProducts(object): # get render elements from the renders for i in range(render_elem_num): renderlayer_name = render_elem.GetRenderElement(i) - target, renderpass = str(renderlayer_name).split(":") - if renderlayer_name.enabled: - for f in range(startFrame, endFrame): - render_element = f"{folder}_{renderpass}.{f}.{fmt}" - render_element = render_element.replace("\\", "/") - render_dict.update({renderpass: render_element}) + if renderlayer_name.enabled or "Cryptomatte" in renderlayer_name: + target, renderpass = str(renderlayer_name).split(":") + render_name.append(renderpass) + return render_name - return render_dict + def get_expected_render_elements(self, folder, name, + startFrame, endFrame, fmt): + """Get all the expected render element output files. """ + render_elements = [] + for f in range(startFrame, endFrame): + render_element = f"{folder}_{name}.{f}.{fmt}" + render_element = render_element.replace("\\", "/") + render_elements.append(render_element) + + return render_elements def image_format(self): return self._project_settings["max"]["RenderSettings"]["image_format"] # noqa diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index c4a44a5b11..1282c9b3fe 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -41,11 +41,11 @@ class CollectRender(pyblish.api.InstancePlugin): full_render_list = beauty_list files_by_aov = { - "_": beauty_list + "beauty": beauty_list } folder = folder.replace("\\", "/") - aovs = RenderProducts().get_aovs() + aovs = RenderProducts().get_aovs(instance.name) files_by_aov.update(aovs) if "expectedFiles" not in instance.data: @@ -78,8 +78,8 @@ class CollectRender(pyblish.api.InstancePlugin): instance.data["attachTo"] = [] data = { - "subset": instance.name, "asset": asset, + "subset": str(instance.name), "publish": True, "maxversion": str(get_max_version()), "imageFormat": img_format, @@ -95,5 +95,3 @@ class CollectRender(pyblish.api.InstancePlugin): } instance.data.update(data) self.log.info("data: {0}".format(data)) - files = instance.data["expectedFiles"] - self.log.debug("expectedFiles: {0}".format(files)) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 68eb0a437d..7133cff058 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -348,7 +348,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): self.log.info("Submitting Deadline job ...") url = "{}/api/jobs".format(self.deadline_url) - response = requests.post(url, json=payload, timeout=10) + response = requests.post(url, json=payload, timeout=10, verify=False) if not response.ok: raise Exception(response.text) From 4303b281aab9c157d109e034c68d5d8816fd4450 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 26 May 2023 19:38:05 +0800 Subject: [PATCH 084/147] hound fix --- openpype/hosts/max/api/lib_renderproducts.py | 4 ++-- openpype/hosts/max/plugins/publish/collect_render.py | 4 +--- .../modules/deadline/plugins/publish/submit_publish_job.py | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index a93a1d821d..b33d0c5751 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -103,7 +103,7 @@ class RenderProducts(object): for name in render_name: render_dict.update({ name: self.get_expected_render_elements( - output_file, name, startFrame, endFrame, img_fmt) + output_file, name, startFrame, endFrame, img_fmt) }) if renderer == "Arnold": @@ -112,7 +112,7 @@ class RenderProducts(object): for name in render_name: render_dict.update({ name: self.get_expected_arnold_product( - output_file, name, startFrame, endFrame, img_fmt) + output_file, name, startFrame, endFrame, img_fmt) }) return render_dict diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 1282c9b3fe..a21ccf532e 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -6,7 +6,7 @@ import pyblish.api from pymxs import runtime as rt from openpype.pipeline import get_current_asset_name from openpype.hosts.max.api import colorspace -from openpype.hosts.max.api.lib import get_max_version, get_current_renderer +from openpype.hosts.max.api.lib import get_max_version from openpype.hosts.max.api.lib_renderproducts import RenderProducts from openpype.client import get_last_version_by_subset_name @@ -29,8 +29,6 @@ class CollectRender(pyblish.api.InstancePlugin): context.data['currentFile'] = current_file asset = get_current_asset_name() - renderer_class = get_current_renderer() - renderer = str(renderer_class).split(":")[0] beauty_list, aov_list = RenderProducts().render_product(instance.name) full_render_list = list() if aov_list: diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 7133cff058..68eb0a437d 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -348,7 +348,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): self.log.info("Submitting Deadline job ...") url = "{}/api/jobs".format(self.deadline_url) - response = requests.post(url, json=payload, timeout=10, verify=False) + response = requests.post(url, json=payload, timeout=10) if not response.ok: raise Exception(response.text) From 6843ae85321ae617dfea564086c5d58fa34f7daf Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 26 May 2023 14:44:47 +0200 Subject: [PATCH 085/147] General: Small code cleanups (#5034) * make sure the message type is set and unset correctly * Update dummy data in readme * remove debug message from main thread callbacks * removed unused import * cleanup code in muster addon * simplified 'get_publish_instance_label' function * even better json file handling Co-authored-by: Roy Nieterau --------- Co-authored-by: Roy Nieterau --- openpype/modules/ftrack/tray/login_dialog.py | 2 -- openpype/modules/muster/muster.py | 14 +++++-------- openpype/pipeline/publish/lib.py | 21 ++++++-------------- openpype/tools/utils/lib.py | 1 - openpype/tools/utils/overlay_messages.py | 3 +-- tests/README.md | 18 ++++++++--------- 6 files changed, 20 insertions(+), 39 deletions(-) diff --git a/openpype/modules/ftrack/tray/login_dialog.py b/openpype/modules/ftrack/tray/login_dialog.py index f374a71178..a8abdaf191 100644 --- a/openpype/modules/ftrack/tray/login_dialog.py +++ b/openpype/modules/ftrack/tray/login_dialog.py @@ -1,5 +1,3 @@ -import os - import requests from qtpy import QtCore, QtGui, QtWidgets diff --git a/openpype/modules/muster/muster.py b/openpype/modules/muster/muster.py index 77b9214a5a..0cdb1230c8 100644 --- a/openpype/modules/muster/muster.py +++ b/openpype/modules/muster/muster.py @@ -1,7 +1,9 @@ import os import json + import appdirs import requests + from openpype.modules import OpenPypeModule, ITrayModule @@ -110,16 +112,10 @@ class MusterModule(OpenPypeModule, ITrayModule): self.save_credentials(token) def save_credentials(self, token): - """ - Save credentials to JSON file - """ - data = { - 'token': token - } + """Save credentials to JSON file.""" - file = open(self.cred_path, 'w') - file.write(json.dumps(data)) - file.close() + with open(self.cred_path, "w") as f: + json.dump({'token': token}, f) def show_login(self): """ diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index f228709b3b..471be5ddb8 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -31,8 +31,6 @@ from .contants import ( TRANSIENT_DIR_TEMPLATE ) -_ARG_PLACEHOLDER = object() - def get_template_name_profiles( project_name, project_settings=None, logger=None @@ -885,31 +883,24 @@ def add_repre_files_for_cleanup(instance, repre): instance.context.data["cleanupFullPaths"].append(expected_file) -def get_publish_instance_label(instance, default=_ARG_PLACEHOLDER): +def get_publish_instance_label(instance): """Try to get label from pyblish instance. - First are checked 'label' and 'name' keys in instance data. If are not set - a default value is returned. Instance object is converted to string - if default value is not specific. + First are used values in instance data under 'label' and 'name' keys. Then + is used string conversion of instance object -> 'instance._name'. Todos: Maybe 'subset' key could be used too. Args: instance (pyblish.api.Instance): Pyblish instance. - default (Optional[Any]): Default value to return if any Returns: - Union[Any]: Instance label or default label. + str: Instance label. """ - label = ( + return ( instance.data.get("label") or instance.data.get("name") + or str(instance) ) - if label: - return label - - if default is _ARG_PLACEHOLDER: - return str(instance) - return default diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index 950c782727..58ece7c68f 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -872,7 +872,6 @@ class WrappedCallbackItem: self.log.warning("- item is already processed") return - self.log.debug("Running callback: {}".format(str(self._callback))) try: result = self._callback(*self._args, **self._kwargs) self._result = result diff --git a/openpype/tools/utils/overlay_messages.py b/openpype/tools/utils/overlay_messages.py index 180d7eae97..4da266bcf7 100644 --- a/openpype/tools/utils/overlay_messages.py +++ b/openpype/tools/utils/overlay_messages.py @@ -127,8 +127,7 @@ class OverlayMessageWidget(QtWidgets.QFrame): if timeout: self._timeout_timer.setInterval(timeout) - if message_type: - set_style_property(self, "type", message_type) + set_style_property(self, "type", message_type) self._timeout_timer.start() diff --git a/tests/README.md b/tests/README.md index d36b6534f8..20847b2449 100644 --- a/tests/README.md +++ b/tests/README.md @@ -15,16 +15,16 @@ Structure: - openpype/modules/MODULE_NAME - structure follow directory structure in code base - fixture - sample data `(MongoDB dumps, test files etc.)` - `tests.py` - single or more pytest files for MODULE_NAME -- unit - quick unit test - - MODULE_NAME +- unit - quick unit test + - MODULE_NAME - fixture - `tests.py` - + How to run: ---------- - use Openpype command 'runtests' from command line (`.venv` in ${OPENPYPE_ROOT} must be activated to use configured Python!) -- `python ${OPENPYPE_ROOT}/start.py runtests` - + By default, this command will run all tests in ${OPENPYPE_ROOT}/tests. Specific location could be provided to this command as an argument, either as absolute path, or relative path to ${OPENPYPE_ROOT}. @@ -41,17 +41,15 @@ In some cases your tests might be so localized, that you don't care about all en In that case you might add this dummy configuration BEFORE any imports in your test file ``` import os -os.environ["AVALON_MONGO"] = "mongodb://localhost:27017" +os.environ["OPENPYPE_DEBUG"] = "1" os.environ["OPENPYPE_MONGO"] = "mongodb://localhost:27017" -os.environ["AVALON_DB"] = "avalon" os.environ["OPENPYPE_DATABASE_NAME"] = "openpype" -os.environ["AVALON_TIMEOUT"] = '3000' -os.environ["OPENPYPE_DEBUG"] = "3" -os.environ["AVALON_CONFIG"] = "pype" +os.environ["AVALON_DB"] = "avalon" +os.environ["AVALON_TIMEOUT"] = "3000" os.environ["AVALON_ASSET"] = "Asset" os.environ["AVALON_PROJECT"] = "test_project" ``` (AVALON_ASSET and AVALON_PROJECT values should exist in your environment) This might be enough to run your test file separately. Do not commit this skeleton though. -Use only when you know what you are doing! \ No newline at end of file +Use only when you know what you are doing! From c14525f371aa8b8b2a524022f860cede764f7d0d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 26 May 2023 23:13:17 +0800 Subject: [PATCH 086/147] fix the wrong directory for rendering --- .../max/plugins/publish/collect_render.py | 2 +- .../deadline/abstract_submit_deadline.py | 2 +- .../plugins/publish/submit_max_deadline.py | 40 +++++++++++-------- .../plugins/publish/submit_publish_job.py | 6 ++- 4 files changed, 30 insertions(+), 20 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index a21ccf532e..c8e407bbe4 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -39,7 +39,7 @@ class CollectRender(pyblish.api.InstancePlugin): full_render_list = beauty_list files_by_aov = { - "beauty": beauty_list + "max_beauty": beauty_list } folder = folder.replace("\\", "/") diff --git a/openpype/modules/deadline/abstract_submit_deadline.py b/openpype/modules/deadline/abstract_submit_deadline.py index 558a637e4b..6694f638d6 100644 --- a/openpype/modules/deadline/abstract_submit_deadline.py +++ b/openpype/modules/deadline/abstract_submit_deadline.py @@ -582,7 +582,7 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin): metadata_folder = metadata_folder.replace(orig_scene, new_scene) instance.data["publishRenderMetadataFolder"] = metadata_folder - + self.log.debug(f"MetadataFolder:{metadata_folder}") self.log.info("Scene name was switched {} -> {}".format( orig_scene, new_scene )) diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index c678c0fb6e..d2de9160fb 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -132,8 +132,8 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, # Add list of expected files to job # --------------------------------- - files = instance.data.get("files") - for filepath in files: + exp = instance.data.get("expectedFiles") + for filepath in self._iter_expected_files(exp): job_info.OutputDirectory += os.path.dirname(filepath) job_info.OutputFilename += os.path.basename(filepath) @@ -162,10 +162,11 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, instance = self._instance filepath = self.scene_path - files = instance.data["files"] + files = instance.data["expectedFiles"] if not files: raise RuntimeError("No Render Elements found!") - output_dir = os.path.dirname(files[0]) + first_file = next(self._iter_expected_files(files)) + output_dir = os.path.dirname(first_file) instance.data["outputDir"] = output_dir instance.data["toBeRenderedOn"] = "deadline" @@ -202,17 +203,11 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, old_output_dir = os.path.dirname(files[0]) output_beauty = RenderSettings().get_render_output(instance.name, old_output_dir) - filepath = self.scene_path - - def _clean_name(path): - return os.path.splitext(os.path.basename(path))[0] - - new_scene = _clean_name(filepath) - orig_scene = _clean_name(instance.context.data["currentFile"]) - - output_beauty = output_beauty.replace(orig_scene, new_scene) - output_beauty = output_beauty.replace("\\", "/") - plugin_data["RenderOutput"] = output_beauty + files = instance.data["expectedFiles"] + first_file = next(self._iter_expected_files(files)) + rgb_bname = os.path.basename(output_beauty) + dir = os.path.dirname(first_file) + plugin_data["RenderOutput"] = f"{dir}/{rgb_bname}" renderer_class = get_current_renderer() renderer = str(renderer_class).split(":")[0] @@ -226,14 +221,25 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, ]: render_elem_list = RenderSettings().get_render_element() for i, element in enumerate(render_elem_list): - element = element.replace(orig_scene, new_scene) - plugin_data["RenderElementOutputFilename%d" % i] = element # noqa + elem_bname = os.path.basename(element) + new_elem = f"{dir}/{elem_bname}" + plugin_data["RenderElementOutputFilename%d" % i] = new_elem # noqa self.log.debug("plugin data:{}".format(plugin_data)) plugin_info.update(plugin_data) return job_info, plugin_info + @staticmethod + def _iter_expected_files(exp): + if isinstance(exp[0], dict): + for _aov, files in exp[0].items(): + for file in files: + yield file + else: + for file in exp: + yield file + @classmethod def get_attribute_defs(cls): defs = super(MaxSubmitDeadline, cls).get_attribute_defs() diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 68eb0a437d..fb0608908f 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -348,7 +348,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): self.log.info("Submitting Deadline job ...") url = "{}/api/jobs".format(self.deadline_url) - response = requests.post(url, json=payload, timeout=10) + response = requests.post(url, json=payload, timeout=10, verify=False) if not response.ok: raise Exception(response.text) @@ -488,11 +488,15 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): if cam: if aov: subset_name = '{}_{}_{}'.format(group_name, cam, aov) + if aov == "max_beauty": + subset_name = '{}_{}'.format(group_name, cam) else: subset_name = '{}_{}'.format(group_name, cam) else: if aov: subset_name = '{}_{}'.format(group_name, aov) + if aov == "max_beauty": + subset_name = '{}'.format(group_name) else: subset_name = '{}'.format(group_name) From 32562e0b39500996bc25ad2173d401b83d60a48f Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Sat, 27 May 2023 00:53:40 +0800 Subject: [PATCH 087/147] give beauty name to RGB --- openpype/hosts/max/plugins/publish/collect_render.py | 2 +- .../modules/deadline/plugins/publish/submit_publish_job.py | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index c8e407bbe4..a21ccf532e 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -39,7 +39,7 @@ class CollectRender(pyblish.api.InstancePlugin): full_render_list = beauty_list files_by_aov = { - "max_beauty": beauty_list + "beauty": beauty_list } folder = folder.replace("\\", "/") diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index fb0608908f..7133cff058 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -488,15 +488,11 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): if cam: if aov: subset_name = '{}_{}_{}'.format(group_name, cam, aov) - if aov == "max_beauty": - subset_name = '{}_{}'.format(group_name, cam) else: subset_name = '{}_{}'.format(group_name, cam) else: if aov: subset_name = '{}_{}'.format(group_name, aov) - if aov == "max_beauty": - subset_name = '{}'.format(group_name) else: subset_name = '{}'.format(group_name) From 271d017bdb368b6cbfdb95087df6da957da43f4a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Sat, 27 May 2023 00:56:11 +0800 Subject: [PATCH 088/147] remove the print function, and set verify to true for payload in publishing job --- openpype/modules/deadline/abstract_submit_deadline.py | 1 - openpype/modules/deadline/plugins/publish/submit_publish_job.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/modules/deadline/abstract_submit_deadline.py b/openpype/modules/deadline/abstract_submit_deadline.py index 6694f638d6..7938c27233 100644 --- a/openpype/modules/deadline/abstract_submit_deadline.py +++ b/openpype/modules/deadline/abstract_submit_deadline.py @@ -582,7 +582,6 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin): metadata_folder = metadata_folder.replace(orig_scene, new_scene) instance.data["publishRenderMetadataFolder"] = metadata_folder - self.log.debug(f"MetadataFolder:{metadata_folder}") self.log.info("Scene name was switched {} -> {}".format( orig_scene, new_scene )) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 7133cff058..68eb0a437d 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -348,7 +348,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): self.log.info("Submitting Deadline job ...") url = "{}/api/jobs".format(self.deadline_url) - response = requests.post(url, json=payload, timeout=10, verify=False) + response = requests.post(url, json=payload, timeout=10) if not response.ok: raise Exception(response.text) From b7b8125d70406ef867af53f8a8afe2640e657058 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 26 May 2023 23:23:06 +0200 Subject: [PATCH 089/147] Use .scriptlib for Resolve startup launch script entry point --- .../hooks/pre_resolve_launch_last_workfile.py | 35 ++++++++++++++++ openpype/hosts/resolve/startup.py | 40 +++++++++++++++++++ .../openpype_startup.scriptlib | 22 ++++++++++ openpype/hosts/resolve/utils.py | 8 ++++ 4 files changed, 105 insertions(+) create mode 100644 openpype/hosts/resolve/hooks/pre_resolve_launch_last_workfile.py create mode 100644 openpype/hosts/resolve/startup.py create mode 100644 openpype/hosts/resolve/utility_scripts/openpype_startup.scriptlib diff --git a/openpype/hosts/resolve/hooks/pre_resolve_launch_last_workfile.py b/openpype/hosts/resolve/hooks/pre_resolve_launch_last_workfile.py new file mode 100644 index 0000000000..6db3cc28b2 --- /dev/null +++ b/openpype/hosts/resolve/hooks/pre_resolve_launch_last_workfile.py @@ -0,0 +1,35 @@ +import os + +from openpype.lib import PreLaunchHook + + +class ResolveLaunchLastWorkfile(PreLaunchHook): + """Special hook to open last workfile for Resolve. + + Checks 'start_last_workfile', if set to False, it will not open last + workfile. This property is set explicitly in Launcher. + """ + + # Execute after workfile template copy + order = 10 + app_groups = ["resolve"] + + def execute(self): + if not self.data.get("start_last_workfile"): + self.log.info("It is set to not start last workfile on start.") + return + + last_workfile = self.data.get("last_workfile_path") + if not last_workfile: + self.log.warning("Last workfile was not collected.") + return + + if not os.path.exists(last_workfile): + self.log.info("Current context does not have any workfile yet.") + return + + # Add path to launch environment for the startup script to pick up + self.log.info(f"Setting OPENPYPE_RESOLVE_OPEN_ON_LAUNCH to launch " + f"last workfile: {last_workfile}") + key = "OPENPYPE_RESOLVE_OPEN_ON_LAUNCH" + self.launch_context.env[key] = last_workfile diff --git a/openpype/hosts/resolve/startup.py b/openpype/hosts/resolve/startup.py new file mode 100644 index 0000000000..4aeb106ef1 --- /dev/null +++ b/openpype/hosts/resolve/startup.py @@ -0,0 +1,40 @@ +import os + +# Importing this takes a little over a second and thus this means +# that we have about 1.5 seconds delay before the workfile will actually +# be opened at the minimum +import openpype.hosts.resolve.api + + +def launch_menu(): + from openpype.pipeline import install_host + print("Launching Resolve OpenPype menu..") + + # Activate resolve from openpype + install_host(openpype.hosts.resolve.api) + + openpype.hosts.resolve.api.launch_pype_menu() + + +def open_file(path): + # Avoid the need to "install" the host + openpype.hosts.resolve.api.bmdvr = resolve # noqa + openpype.hosts.resolve.api.bmdvf = fusion # noqa + openpype.hosts.resolve.api.open_file(path) + + +def main(): + # Open last workfile + workfile_path = os.environ.get("OPENPYPE_RESOLVE_OPEN_ON_LAUNCH") + if workfile_path: + open_file(workfile_path) + else: + print("No last workfile set to open. Skipping..") + + # Launch OpenPype menu + # TODO: Add a setting to enable/disable this + launch_menu() + + +if __name__ == "__main__": + main() diff --git a/openpype/hosts/resolve/utility_scripts/openpype_startup.scriptlib b/openpype/hosts/resolve/utility_scripts/openpype_startup.scriptlib new file mode 100644 index 0000000000..9fca666d78 --- /dev/null +++ b/openpype/hosts/resolve/utility_scripts/openpype_startup.scriptlib @@ -0,0 +1,22 @@ +-- Run OpenPype's Python launch script for resolve +function file_exists(name) + local f = io.open(name, "r") + return f ~= nil and io.close(f) +end + + +openpype_root = os.getenv("OPENPYPE_ROOT") +if openpype_root ~= nil then + script = openpype_root .. "/openpype/hosts/resolve/startup.py" + script = fusion:MapPath(script) + + if file_exists(script) then + -- We must use RunScript to ensure it runs in a separate + -- process to Resolve itself to avoid a deadlock for + -- certain imports of OpenPype libraries or Qt + print("Running launch script: " .. script) + fusion:RunScript(script) + else + print("Launch script not found at: " .. script) + end +end \ No newline at end of file diff --git a/openpype/hosts/resolve/utils.py b/openpype/hosts/resolve/utils.py index 9a161f4865..e2c8c4a05e 100644 --- a/openpype/hosts/resolve/utils.py +++ b/openpype/hosts/resolve/utils.py @@ -50,6 +50,14 @@ def setup(env): src = os.path.join(directory, script) dst = os.path.join(util_scripts_dir, script) + + # TODO: Make this a less hacky workaround + if script == "openpype_startup.scriptlib": + # Handle special case for scriptlib that needs to be a folder + # up from the Comp folder in the Fusion scripts + dst = os.path.join(os.path.dirname(util_scripts_dir), + script) + log.info("Copying `{}` to `{}`...".format(src, dst)) if os.path.isdir(src): shutil.copytree( From 00e89719dd75f465411e120fbb2c37dd8e495b7a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 26 May 2023 23:23:33 +0200 Subject: [PATCH 090/147] Do not prompt save project when not in a project (e.g. on Resolve launch) --- openpype/hosts/resolve/api/workio.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/resolve/api/workio.py b/openpype/hosts/resolve/api/workio.py index 5ce73eea53..5966fa6a43 100644 --- a/openpype/hosts/resolve/api/workio.py +++ b/openpype/hosts/resolve/api/workio.py @@ -43,18 +43,22 @@ def open_file(filepath): """ Loading project """ + + from . import bmdvr + pm = get_project_manager() + page = bmdvr.GetCurrentPage() + if page is not None: + # Save current project only if Resolve has an active page, otherwise + # we consider Resolve being in a pre-launch state (no open UI yet) + project = pm.GetCurrentProject() + print(f"Saving current project: {project}") + pm.SaveProject() + file = os.path.basename(filepath) fname, _ = os.path.splitext(file) dname, _ = fname.split("_v") - - # deal with current project - project = pm.GetCurrentProject() - log.info(f"Test `pm`: {pm}") - pm.SaveProject() - try: - log.info(f"Test `dname`: {dname}") if not set_project_manager_to_folder_name(dname): raise # load project from input path @@ -72,6 +76,7 @@ def open_file(filepath): return False return True + def current_file(): pm = get_project_manager() current_dir = os.getenv("AVALON_WORKDIR") From eb9d8942460f3640c9aeabd63e8fdd45d2e2e955 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 27 May 2023 03:25:05 +0000 Subject: [PATCH 091/147] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index 342bbfc85a..c24388b2ff 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.8" +__version__ = "3.15.9-nightly.1" From f8cb017e90490b80fb6f6470db685090a23e7211 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 27 May 2023 03:25:45 +0000 Subject: [PATCH 092/147] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 4d7d06a2c8..54a4ee6ac0 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.15.9-nightly.1 - 3.15.8 - 3.15.8-nightly.3 - 3.15.8-nightly.2 @@ -134,7 +135,6 @@ body: - 3.14.2-nightly.5 - 3.14.2-nightly.4 - 3.14.2-nightly.3 - - 3.14.2-nightly.2 validations: required: true - type: dropdown From 56642ac17572ca5c7c12bd97e5e717ce801518f0 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 12:37:25 +0800 Subject: [PATCH 093/147] getting the filename from render settings and add save_scene before all the extractors running --- openpype/hosts/max/api/lib_renderproducts.py | 159 ++++++------------ .../hosts/max/plugins/create/create_render.py | 4 + .../max/plugins/publish/collect_render.py | 26 +-- .../hosts/max/plugins/publish/save_scene.py | 26 +++ .../plugins/publish/submit_max_deadline.py | 81 ++++++++- 5 files changed, 171 insertions(+), 125 deletions(-) create mode 100644 openpype/hosts/max/plugins/publish/save_scene.py diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index b33d0c5751..30c3c71cce 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -5,10 +5,8 @@ import os from pymxs import runtime as rt from openpype.hosts.max.api.lib import ( - get_current_renderer, - get_default_render_folder + get_current_renderer ) -from openpype.pipeline.context_tools import get_current_project_asset from openpype.settings import get_project_settings from openpype.pipeline import legacy_io @@ -22,66 +20,30 @@ class RenderProducts(object): legacy_io.Session["AVALON_PROJECT"] ) - def render_product(self, container): - folder = rt.maxFilePath - file = rt.maxFileName - folder = folder.replace("\\", "/") - setting = self._project_settings - render_folder = get_default_render_folder(setting) - filename, ext = os.path.splitext(file) + def get_beauty(self, container): + render_dir = os.path.dirname(rt.rendOutputFilename) - output_file = os.path.join(folder, - render_folder, - filename, + output_file = os.path.join(render_dir, container) - # TODO: change the frame range follows the current render setting + + setting = self._project_settings + img_fmt = setting["max"]["RenderSettings"]["image_format"] # noqa + startFrame = int(rt.rendStart) endFrame = int(rt.rendEnd) + 1 - img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] # noqa - rgba_render_list = self.beauty_render_product(output_file, - startFrame, - endFrame, - img_fmt) - - renderer_class = get_current_renderer() - renderer = str(renderer_class).split(":")[0] - - render_elem_list = None - - if renderer in [ - "ART_Renderer", - "Redshift_Renderer", - "V_Ray_6_Hotfix_3", - "V_Ray_GPU_6_Hotfix_3", - "Default_Scanline_Renderer", - "Quicksilver_Hardware_Renderer", - ]: - render_elem_list = self.render_elements_product(output_file, - startFrame, - endFrame, - img_fmt) - - if renderer == "Arnold": - render_elem_list = self.arnold_render_product(output_file, - startFrame, - endFrame, - img_fmt) - - return rgba_render_list, render_elem_list + render_dict = { + "beauty": self.get_expected_beauty( + output_file, startFrame, endFrame, img_fmt) + } + return render_dict def get_aovs(self, container): - folder = rt.maxFilePath - file = rt.maxFileName - folder = folder.replace("\\", "/") - setting = self._project_settings - render_folder = get_default_render_folder(setting) - filename, ext = os.path.splitext(file) + render_dir = os.path.dirname(rt.rendOutputFilename) - output_file = os.path.join(folder, - render_folder, - filename, + output_file = os.path.join(render_dir, container) + setting = self._project_settings img_fmt = setting["max"]["RenderSettings"]["image_format"] # noqa @@ -90,9 +52,9 @@ class RenderProducts(object): renderer_class = get_current_renderer() renderer = str(renderer_class).split(":")[0] render_dict = {} + if renderer in [ "ART_Renderer", - "Redshift_Renderer", "V_Ray_6_Hotfix_3", "V_Ray_GPU_6_Hotfix_3", "Default_Scanline_Renderer", @@ -105,6 +67,23 @@ class RenderProducts(object): name: self.get_expected_render_elements( output_file, name, startFrame, endFrame, img_fmt) }) + if renderer == "Redshift_Renderer": + render_name = self.get_render_elements_name() + if render_name: + rs_AovFiles = rt.Redshift_Renderer().SeparateAovFiles + if rs_AovFiles != True and img_fmt == "exr": + for name in render_name: + if name == "RsCryptomatte": + render_dict.update({ + name: self.get_expected_render_elements( + output_file, name, startFrame, endFrame, img_fmt) + }) + else: + for name in render_name: + render_dict.update({ + name: self.get_expected_render_elements( + output_file, name, startFrame, endFrame, img_fmt) + }) if renderer == "Arnold": render_name = self.get_arnold_product_name() @@ -114,60 +93,31 @@ class RenderProducts(object): name: self.get_expected_arnold_product( output_file, name, startFrame, endFrame, img_fmt) }) + if renderer in [ + "V_Ray_6_Hotfix_3", + "V_Ray_GPU_6_Hotfix_3" + ]: + if img_fmt !="exr": + render_name = self.get_render_elements_name() + if render_name: + for name in render_name: + render_dict.update({ + name: self.get_expected_render_elements( + output_file, name, startFrame, endFrame, img_fmt) + }) return render_dict - def beauty_render_product(self, folder, startFrame, endFrame, fmt): + def get_expected_beauty(self, folder, startFrame, endFrame, fmt): beauty_frame_range = [] for f in range(startFrame, endFrame): - beauty_output = f"{folder}.{f}.{fmt}" + frame = "%04d" % f + beauty_output = f"{folder}.{frame}.{fmt}" beauty_output = beauty_output.replace("\\", "/") beauty_frame_range.append(beauty_output) return beauty_frame_range - # TODO: Get the arnold render product - def arnold_render_product(self, folder, startFrame, endFrame, fmt): - """Get all the Arnold AOVs""" - aovs = [] - - amw = rt.MaxtoAOps.AOVsManagerWindow() - aov_mgr = rt.renderers.current.AOVManager - # Check if there is any aov group set in AOV manager - aov_group_num = len(aov_mgr.drivers) - if aov_group_num < 1: - return - for i in range(aov_group_num): - # get the specific AOV group - for aov in aov_mgr.drivers[i].aov_list: - for f in range(startFrame, endFrame): - render_element = f"{folder}_{aov.name}.{f}.{fmt}" - render_element = render_element.replace("\\", "/") - aovs.append(render_element) - - # close the AOVs manager window - amw.close() - - return aovs - - def render_elements_product(self, folder, startFrame, endFrame, fmt): - """Get all the render element output files. """ - render_dirname = [] - - render_elem = rt.maxOps.GetCurRenderElementMgr() - render_elem_num = render_elem.NumRenderElements() - # get render elements from the renders - for i in range(render_elem_num): - renderlayer_name = render_elem.GetRenderElement(i) - target, renderpass = str(renderlayer_name).split(":") - if renderlayer_name.enabled: - for f in range(startFrame, endFrame): - render_element = f"{folder}_{renderpass}.{f}.{fmt}" - render_element = render_element.replace("\\", "/") - render_dirname.append(render_element) - - return render_dirname - def get_arnold_product_name(self): """Get all the Arnold AOVs name""" aov_name = [] @@ -193,14 +143,15 @@ class RenderProducts(object): """Get all the expected Arnold AOVs""" aov_list = [] for f in range(startFrame, endFrame): - render_element = f"{folder}_{name}.{f}.{fmt}" + frame = "%04d" % f + render_element = f"{folder}_{name}.{frame}.{fmt}" render_element = render_element.replace("\\", "/") aov_list.append(render_element) return aov_list def get_render_elements_name(self): - """Get all the render element names. """ + """Get all the render element names for general """ render_name = [] render_elem = rt.maxOps.GetCurRenderElementMgr() render_elem_num = render_elem.NumRenderElements() @@ -209,9 +160,10 @@ class RenderProducts(object): # get render elements from the renders for i in range(render_elem_num): renderlayer_name = render_elem.GetRenderElement(i) - if renderlayer_name.enabled or "Cryptomatte" in renderlayer_name: + if renderlayer_name.enabled: target, renderpass = str(renderlayer_name).split(":") render_name.append(renderpass) + return render_name def get_expected_render_elements(self, folder, name, @@ -219,7 +171,8 @@ class RenderProducts(object): """Get all the expected render element output files. """ render_elements = [] for f in range(startFrame, endFrame): - render_element = f"{folder}_{name}.{f}.{fmt}" + frame = "%04d" % f + render_element = f"{folder}_{name}.{frame}.{fmt}" render_element = render_element.replace("\\", "/") render_elements.append(render_element) diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index 68ae5eac72..78e9527bdf 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- """Creator plugin for creating camera.""" +import os from openpype.hosts.max.api import plugin from openpype.pipeline import CreatedInstance from openpype.hosts.max.api.lib_rendersettings import RenderSettings @@ -14,6 +15,9 @@ class CreateRender(plugin.MaxCreator): def create(self, subset_name, instance_data, pre_create_data): from pymxs import runtime as rt sel_obj = list(rt.selection) + file = rt.maxFileName + filename, _ = os.path.splitext(file) + instance_data["AssetName"] = filename instance = super(CreateRender, self).create( subset_name, instance_data, diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index a21ccf532e..5b3f99b2d0 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -29,19 +29,7 @@ class CollectRender(pyblish.api.InstancePlugin): context.data['currentFile'] = current_file asset = get_current_asset_name() - beauty_list, aov_list = RenderProducts().render_product(instance.name) - full_render_list = list() - if aov_list: - full_render_list.extend(iter(beauty_list)) - full_render_list.extend(iter(aov_list)) - - else: - full_render_list = beauty_list - - files_by_aov = { - "beauty": beauty_list - } - + files_by_aov = RenderProducts().get_beauty(instance.name) folder = folder.replace("\\", "/") aovs = RenderProducts().get_aovs(instance.name) files_by_aov.update(aovs) @@ -67,14 +55,14 @@ class CollectRender(pyblish.api.InstancePlugin): # OCIO config not support in # most of the 3dsmax renderers # so this is currently hard coded - setting = instance.context.data["project_settings"] - image_io = setting["global"]["imageio"] - instance.data["colorspaceConfig"] = image_io["ocio_config"]["filepath"][0] # noqa + # TODO: add options for redshift/vray ocio config + instance.data["colorspaceConfig"] = "" instance.data["colorspaceDisplay"] = "sRGB" - instance.data["colorspaceView"] = "ACES 1.0" + instance.data["colorspaceView"] = "ACES 1.0 SDR-video" instance.data["renderProducts"] = colorspace.ARenderProduct() + instance.data["publishJobState"] = "Suspended" instance.data["attachTo"] = [] - + # also need to get the render dir for coversion data = { "asset": asset, "subset": str(instance.name), @@ -84,7 +72,6 @@ class CollectRender(pyblish.api.InstancePlugin): "family": 'maxrender', "families": ['maxrender'], "source": filepath, - "files": full_render_list, "plugin": "3dsmax", "frameStart": int(rt.rendStart), "frameEnd": int(rt.rendEnd), @@ -93,3 +80,4 @@ class CollectRender(pyblish.api.InstancePlugin): } instance.data.update(data) self.log.info("data: {0}".format(data)) + self.log.debug("expectedFiles:{0}".format(instance.data["expectedFiles"])) diff --git a/openpype/hosts/max/plugins/publish/save_scene.py b/openpype/hosts/max/plugins/publish/save_scene.py new file mode 100644 index 0000000000..93d97a3de5 --- /dev/null +++ b/openpype/hosts/max/plugins/publish/save_scene.py @@ -0,0 +1,26 @@ +import pyblish.api +import os + + +class SaveCurrentScene(pyblish.api.ContextPlugin): + """Save current scene + + """ + + label = "Save current file" + order = pyblish.api.ExtractorOrder - 0.49 + hosts = ["max"] + families = ["maxrender", "workfile"] + + def process(self, context): + from pymxs import runtime as rt + folder = rt.maxFilePath + file = rt.maxFileName + current = os.path.join(folder, file) + assert context.data["currentFile"] == current + + if rt.checkForSave(): + self.log.debug("Skipping file save as there " + "are no modifications..") + return + rt.saveMaxFile(current) diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index d2de9160fb..15aa521f43 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -78,7 +78,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, job_info.BatchName = src_filename job_info.Plugin = instance.data["plugin"] job_info.UserName = context.data.get("deadlineUser", getpass.getuser()) - + job_info.EnableAutoTimeout = True # Deadline requires integers in frame range frames = "{start}-{end}".format( start=int(instance.data["frameStart"]), @@ -207,9 +207,13 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, first_file = next(self._iter_expected_files(files)) rgb_bname = os.path.basename(output_beauty) dir = os.path.dirname(first_file) - plugin_data["RenderOutput"] = f"{dir}/{rgb_bname}" - + beauty_name = f"{dir}/{rgb_bname}" + beauty_name = beauty_name.replace("\\", "/") + plugin_data["RenderOutput"] = beauty_name + # as 3dsmax has version with different languages + plugin_data["Language"] = "ENU" renderer_class = get_current_renderer() + renderer = str(renderer_class).split(":")[0] if renderer in [ "ART_Renderer", @@ -223,13 +227,84 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, for i, element in enumerate(render_elem_list): elem_bname = os.path.basename(element) new_elem = f"{dir}/{elem_bname}" + new_elem = new_elem.replace("/", "\\") plugin_data["RenderElementOutputFilename%d" % i] = new_elem # noqa + self.log.debug("plugin data:{}".format(plugin_data)) plugin_info.update(plugin_data) return job_info, plugin_info + def from_published_scene(self, replace_in_path=True): + instance = self._instance + workfile_instance = self._get_workfile_instance(instance.context) + if workfile_instance is None: + return + + # determine published path from Anatomy. + template_data = workfile_instance.data.get("anatomyData") + rep = workfile_instance.data["representations"][0] + template_data["representation"] = rep.get("name") + template_data["ext"] = rep.get("ext") + template_data["comment"] = None + + anatomy = instance.context.data['anatomy'] + template_obj = anatomy.templates_obj["publish"]["path"] + template_filled = template_obj.format_strict(template_data) + file_path = os.path.normpath(template_filled) + + self.log.info("Using published scene for render {}".format(file_path)) + + if not os.path.exists(file_path): + self.log.error("published scene does not exist!") + raise + + if not replace_in_path: + return file_path + + # now we need to switch scene in expected files + # because token will now point to published + # scene file and that might differ from current one + def _clean_name(path): + return os.path.splitext(os.path.basename(path))[0] + + new_scene = _clean_name(file_path) + orig_scene = _clean_name(instance.data["AssetName"]) + expected_files = instance.data.get("expectedFiles") + + if isinstance(expected_files[0], dict): + # we have aovs and we need to iterate over them + new_exp = {} + for aov, files in expected_files[0].items(): + replaced_files = [] + for f in files: + replaced_files.append( + str(f).replace(orig_scene, new_scene) + ) + new_exp[aov] = replaced_files + # [] might be too much here, TODO + instance.data["expectedFiles"] = [new_exp] + else: + new_exp = [] + for f in expected_files: + new_exp.append( + str(f).replace(orig_scene, new_scene) + ) + instance.data["expectedFiles"] = new_exp + + metadata_folder = instance.data.get("publishRenderMetadataFolder") + if metadata_folder: + metadata_folder = metadata_folder.replace(orig_scene, + new_scene) + instance.data["publishRenderMetadataFolder"] = metadata_folder + + self.log.info("Scene name was switched {} -> {}".format( + orig_scene, new_scene + )) + + return file_path + @staticmethod def _iter_expected_files(exp): if isinstance(exp[0], dict): From f85051863ccfbd2f54e0f9198c1b3123f08391b3 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 12:44:22 +0800 Subject: [PATCH 094/147] hound fix --- openpype/hosts/max/api/lib_renderproducts.py | 15 +++++++++------ .../hosts/max/plugins/publish/collect_render.py | 1 - .../plugins/publish/submit_max_deadline.py | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 30c3c71cce..68090dfefd 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -65,7 +65,8 @@ class RenderProducts(object): for name in render_name: render_dict.update({ name: self.get_expected_render_elements( - output_file, name, startFrame, endFrame, img_fmt) + output_file, name, startFrame, + endFrame, img_fmt) }) if renderer == "Redshift_Renderer": render_name = self.get_render_elements_name() @@ -76,13 +77,15 @@ class RenderProducts(object): if name == "RsCryptomatte": render_dict.update({ name: self.get_expected_render_elements( - output_file, name, startFrame, endFrame, img_fmt) + output_file, name, startFrame, + endFrame, img_fmt) }) else: for name in render_name: render_dict.update({ name: self.get_expected_render_elements( - output_file, name, startFrame, endFrame, img_fmt) + output_file, name, startFrame, + endFrame, img_fmt) }) if renderer == "Arnold": @@ -95,15 +98,15 @@ class RenderProducts(object): }) if renderer in [ "V_Ray_6_Hotfix_3", - "V_Ray_GPU_6_Hotfix_3" - ]: + "V_Ray_GPU_6_Hotfix_3"]: if img_fmt !="exr": render_name = self.get_render_elements_name() if render_name: for name in render_name: render_dict.update({ name: self.get_expected_render_elements( - output_file, name, startFrame, endFrame, img_fmt) + output_file, name, startFrame, + endFrame, img_fmt) # noqa }) return render_dict diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 5b3f99b2d0..9137f8c854 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -80,4 +80,3 @@ class CollectRender(pyblish.api.InstancePlugin): } instance.data.update(data) self.log.info("data: {0}".format(data)) - self.log.debug("expectedFiles:{0}".format(instance.data["expectedFiles"])) diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index 15aa521f43..4682cc4487 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -207,7 +207,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, first_file = next(self._iter_expected_files(files)) rgb_bname = os.path.basename(output_beauty) dir = os.path.dirname(first_file) - beauty_name = f"{dir}/{rgb_bname}" + beauty_name = f"{dir}/{rgb_bname}" beauty_name = beauty_name.replace("\\", "/") plugin_data["RenderOutput"] = beauty_name # as 3dsmax has version with different languages From b89bb229a0e7bf4c8d8308776d3d288a65bdd387 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 12:48:09 +0800 Subject: [PATCH 095/147] hound --- openpype/hosts/max/api/lib_renderproducts.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 68090dfefd..21c41446a9 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -72,7 +72,7 @@ class RenderProducts(object): render_name = self.get_render_elements_name() if render_name: rs_AovFiles = rt.Redshift_Renderer().SeparateAovFiles - if rs_AovFiles != True and img_fmt == "exr": + if rs_AovFiles == False and img_fmt == "exr": for name in render_name: if name == "RsCryptomatte": render_dict.update({ @@ -98,7 +98,8 @@ class RenderProducts(object): }) if renderer in [ "V_Ray_6_Hotfix_3", - "V_Ray_GPU_6_Hotfix_3"]: + "V_Ray_GPU_6_Hotfix_3" + ]: if img_fmt !="exr": render_name = self.get_render_elements_name() if render_name: From f030ca17d8a3a7d979f01f3b56b11732d6dcfb85 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 12:50:36 +0800 Subject: [PATCH 096/147] hound --- openpype/hosts/max/api/lib_renderproducts.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 21c41446a9..6bd7e5b7b0 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -72,14 +72,14 @@ class RenderProducts(object): render_name = self.get_render_elements_name() if render_name: rs_AovFiles = rt.Redshift_Renderer().SeparateAovFiles - if rs_AovFiles == False and img_fmt == "exr": + if img_fmt == "exr" and not rs_AovFiles: for name in render_name: if name == "RsCryptomatte": render_dict.update({ - name: self.get_expected_render_elements( - output_file, name, startFrame, - endFrame, img_fmt) - }) + name: self.get_expected_render_elements( + output_file, name, startFrame, + endFrame, img_fmt) + }) else: for name in render_name: render_dict.update({ @@ -99,7 +99,7 @@ class RenderProducts(object): if renderer in [ "V_Ray_6_Hotfix_3", "V_Ray_GPU_6_Hotfix_3" - ]: + ]: if img_fmt !="exr": render_name = self.get_render_elements_name() if render_name: From 44c0f1cf8a52a5eb1b45d43310f2451aa966be01 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 12:51:26 +0800 Subject: [PATCH 097/147] hound --- openpype/hosts/max/api/lib_renderproducts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 6bd7e5b7b0..2fbb7e8ff3 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -100,7 +100,7 @@ class RenderProducts(object): "V_Ray_6_Hotfix_3", "V_Ray_GPU_6_Hotfix_3" ]: - if img_fmt !="exr": + if img_fmt != "exr": render_name = self.get_render_elements_name() if render_name: for name in render_name: From 0fccdaf5f5d46e775863193ff7f3a44e3338eda9 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 15:54:55 +0800 Subject: [PATCH 098/147] use expected files instead of files --- .../modules/deadline/plugins/publish/submit_max_deadline.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index 4682cc4487..3fde667dfe 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -197,10 +197,11 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, else: plugin_data["DisableMultipass"] = 1 - files = instance.data.get("files") + files = instance.data.get("expectedFiles") if not files: raise RuntimeError("No render elements found") - old_output_dir = os.path.dirname(files[0]) + first_file = next(self._iter_expected_files(files)) + old_output_dir = os.path.dirname(first_file) output_beauty = RenderSettings().get_render_output(instance.name, old_output_dir) files = instance.data["expectedFiles"] From 66070377104142edce133201389e896774d1f3f9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 29 May 2023 11:25:14 +0200 Subject: [PATCH 099/147] Publisher: Call explicitly prepared tab methods (#5044) * call explicitly prepared tab methods * add an overlay message --- openpype/tools/publisher/window.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index fc90e66f21..6ab444109e 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -676,7 +676,15 @@ class PublisherWindow(QtWidgets.QDialog): self._tabs_widget.set_current_tab(identifier) def set_current_tab(self, tab): - self._set_current_tab(tab) + if tab == "create": + self._go_to_create_tab() + elif tab == "publish": + self._go_to_publish_tab() + elif tab == "report": + self._go_to_report_tab() + elif tab == "details": + self._go_to_details_tab() + if not self._window_is_visible: self.set_tab_on_reset(tab) @@ -686,6 +694,12 @@ class PublisherWindow(QtWidgets.QDialog): def _go_to_create_tab(self): if self._create_tab.isEnabled(): self._set_current_tab("create") + return + + self._overlay_object.add_message( + "Can't switch to Create tab because publishing is paused.", + message_type="info" + ) def _go_to_publish_tab(self): self._set_current_tab("publish") From a4fd3c7c6dae31509fc37621878aa4600933f2ae Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 29 May 2023 11:50:56 +0100 Subject: [PATCH 100/147] Minor refactor --- .../hosts/unreal/plugins/load/load_camera.py | 45 +++++++------------ 1 file changed, 15 insertions(+), 30 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index c4fe9df70b..af7a594b41 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -89,10 +89,7 @@ class CameraLoader(plugin.Loader): hierarchy_dir_list.append(hierarchy_dir) asset = context.get('asset').get('name') suffix = "_CON" - if asset: - asset_name = "{}_{}".format(asset, name) - else: - asset_name = "{}".format(name) + asset_name = f"{asset}_{name}" if asset else f"{name}" tools = unreal.AssetToolsHelpers().get_asset_tools() @@ -106,23 +103,15 @@ class CameraLoader(plugin.Loader): # Get highest number to make a unique name folders = [a for a in asset_content if a[-1] == "/" and f"{name}_" in a] - f_numbers = [] - for f in folders: - # Get number from folder name. Splits the string by "_" and - # removes the last element (which is a "/"). - f_numbers.append(int(f.split("_")[-1][:-1])) + # Get number from folder name. Splits the string by "_" and + # removes the last element (which is a "/"). + f_numbers = [int(f.split("_")[-1][:-1]) for f in folders] f_numbers.sort() - if not f_numbers: - unique_number = 1 - else: - unique_number = f_numbers[-1] + 1 + unique_number = f_numbers[-1] + 1 if f_numbers else 1 asset_dir, container_name = tools.create_unique_asset_name( f"{hierarchy_dir}/{asset}/{name}_{unique_number:02d}", suffix="") - asset_path = Path(asset_dir) - asset_path_parent = str(asset_path.parent.as_posix()) - container_name += suffix EditorAssetLibrary.make_directory(asset_dir) @@ -163,17 +152,17 @@ class CameraLoader(plugin.Loader): asset).get_class().get_name() == 'LevelSequence' ] - if not existing_sequences: + if existing_sequences: + for seq in existing_sequences: + sequences.append(seq.get_asset()) + frame_ranges.append(( + seq.get_asset().get_playback_start(), + seq.get_asset().get_playback_end())) + else: sequence, frame_range = generate_sequence(h, h_dir) sequences.append(sequence) frame_ranges.append(frame_range) - else: - for e in existing_sequences: - sequences.append(e.get_asset()) - frame_ranges.append(( - e.get_asset().get_playback_start(), - e.get_asset().get_playback_end())) EditorAssetLibrary.make_directory(asset_dir) @@ -252,8 +241,7 @@ class CameraLoader(plugin.Loader): "parent": context["representation"]["parent"], "family": context["representation"]["context"]["family"] } - imprint( - "{}/{}".format(asset_dir, container_name), data) + imprint(f"{asset_dir}/{container_name}", data) EditorLevelLibrary.save_all_dirty_levels() EditorLevelLibrary.load_level(master_level) @@ -415,8 +403,7 @@ class CameraLoader(plugin.Loader): "representation": str(representation["_id"]), "parent": str(representation["parent"]) } - imprint( - "{}/{}".format(asset_dir, container.get('container_name')), data) + imprint(f"{asset_dir}/{container.get('container_name')}", data) EditorLevelLibrary.save_current_level() @@ -514,10 +501,8 @@ class CameraLoader(plugin.Loader): break sequences.append(ss.get_sequence()) # Update subscenes indexes. - i = 0 - for ss in sections: + for i, ss in enumerate(sections): ss.set_row_index(i) - i += 1 if visibility_track: sections = visibility_track.get_sections() From 79a0210c35eda89c355afd0497d2ae14fa9bcb45 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 29 May 2023 11:56:03 +0100 Subject: [PATCH 101/147] Save whole hierarchy when loading camera or layout --- openpype/hosts/unreal/plugins/load/load_camera.py | 5 +++-- openpype/hosts/unreal/plugins/load/load_layout.py | 11 ++++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index af7a594b41..3ed7b055e3 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -246,8 +246,9 @@ class CameraLoader(plugin.Loader): EditorLevelLibrary.save_all_dirty_levels() EditorLevelLibrary.load_level(master_level) + # Save all assets in the hierarchy asset_content = EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=True + hierarchy_dir_list[0], recursive=True, include_folder=False ) for a in asset_content: @@ -408,7 +409,7 @@ class CameraLoader(plugin.Loader): EditorLevelLibrary.save_current_level() asset_content = EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=False) + f"{root}/{ms_asset}", recursive=True, include_folder=False) for a in asset_content: EditorAssetLibrary.save_asset(a) diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index 5a3953f82e..51ca0383e0 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -642,8 +642,10 @@ class LayoutLoader(plugin.Loader): imprint( "{}/{}".format(asset_dir, container_name), data) + save_dir = hierarchy_dir_list[0] if create_sequences else asset_dir + asset_content = EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=False) + save_dir, recursive=True, include_folder=False) for a in asset_content: EditorAssetLibrary.save_asset(a) @@ -664,11 +666,12 @@ class LayoutLoader(plugin.Loader): asset_dir = container.get('namespace') context = representation.get("context") + hierarchy = context.get('hierarchy').split("/") + sequence = None master_level = None if create_sequences: - hierarchy = context.get('hierarchy').split("/") h_dir = f"{root}/{hierarchy[0]}" h_asset = hierarchy[0] master_level = f"{h_dir}/{h_asset}_map.{h_asset}_map" @@ -726,8 +729,10 @@ class LayoutLoader(plugin.Loader): EditorLevelLibrary.save_current_level() + save_dir = f"{root}/{hierarchy[0]}" if create_sequences else asset_dir + asset_content = EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=False) + save_dir, recursive=True, include_folder=False) for a in asset_content: EditorAssetLibrary.save_asset(a) From ff2c494705f79d21b75dcfc77fb2c1a49b23bae8 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 29 May 2023 12:10:23 +0100 Subject: [PATCH 102/147] Set view range in sequencer when creating sequences --- openpype/hosts/unreal/api/pipeline.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/api/pipeline.py b/openpype/hosts/unreal/api/pipeline.py index 5030e8ee86..72816c9b81 100644 --- a/openpype/hosts/unreal/api/pipeline.py +++ b/openpype/hosts/unreal/api/pipeline.py @@ -623,11 +623,18 @@ def generate_sequence(h, h_dir): min_frame = min(start_frames) max_frame = max(end_frames) + fps = asset_data.get('data').get("fps") + sequence.set_display_rate( - unreal.FrameRate(asset_data.get('data').get("fps"), 1.0)) + unreal.FrameRate(fps, 1.0)) sequence.set_playback_start(min_frame) sequence.set_playback_end(max_frame) + sequence.set_work_range_start(min_frame / fps) + sequence.set_work_range_end(max_frame / fps) + sequence.set_view_range_start(min_frame / fps) + sequence.set_view_range_end(max_frame / fps) + tracks = sequence.get_master_tracks() track = None for t in tracks: From 9d9eebfe09499474dac3504d7c4b252100e8c57d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 19:25:49 +0800 Subject: [PATCH 103/147] using current file to render and add validator for deadline publish in max hosts --- openpype/hosts/max/api/lib_renderproducts.py | 4 +- .../hosts/max/plugins/create/create_render.py | 14 ++++ .../max/plugins/publish/collect_render.py | 9 ++- .../hosts/max/plugins/publish/save_scene.py | 5 -- .../publish/validate_deadline_publish.py | 41 ++++++++++ .../plugins/publish/submit_max_deadline.py | 76 ++----------------- 6 files changed, 72 insertions(+), 77 deletions(-) create mode 100644 openpype/hosts/max/plugins/publish/validate_deadline_publish.py diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 2fbb7e8ff3..7e016f6f15 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -38,7 +38,7 @@ class RenderProducts(object): } return render_dict - def get_aovs(self, container): + def get_aovs(self, container, instance): render_dir = os.path.dirname(rt.rendOutputFilename) output_file = os.path.join(render_dir, @@ -71,7 +71,7 @@ class RenderProducts(object): if renderer == "Redshift_Renderer": render_name = self.get_render_elements_name() if render_name: - rs_AovFiles = rt.Redshift_Renderer().SeparateAovFiles + rs_AovFiles = instance.data.get("separateAovFiles") if img_fmt == "exr" and not rs_AovFiles: for name in render_name: if name == "RsCryptomatte": diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index 78e9527bdf..3d5ed781a4 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -2,6 +2,7 @@ """Creator plugin for creating camera.""" import os from openpype.hosts.max.api import plugin +from openpype.lib import BoolDef from openpype.pipeline import CreatedInstance from openpype.hosts.max.api.lib_rendersettings import RenderSettings @@ -18,6 +19,9 @@ class CreateRender(plugin.MaxCreator): file = rt.maxFileName filename, _ = os.path.splitext(file) instance_data["AssetName"] = filename + instance_data["separateAovFiles"] = ( + pre_create_data.get("separateAovFiles")) + instance = super(CreateRender, self).create( subset_name, instance_data, @@ -40,3 +44,13 @@ class CreateRender(plugin.MaxCreator): RenderSettings().set_render_camera(sel_obj) # set output paths for rendering(mandatory for deadline) RenderSettings().render_output(container_name) + + def get_pre_create_attr_defs(self): + attrs = super(CreateRender, self).get_pre_create_attr_defs() + + return attrs + [ + BoolDef("separateAovFiles", + label="Separate Aov Files", + default=False), + + ] diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 9137f8c854..14d23c42fc 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -6,7 +6,7 @@ import pyblish.api from pymxs import runtime as rt from openpype.pipeline import get_current_asset_name from openpype.hosts.max.api import colorspace -from openpype.hosts.max.api.lib import get_max_version +from openpype.hosts.max.api.lib import get_max_version, get_current_renderer from openpype.hosts.max.api.lib_renderproducts import RenderProducts from openpype.client import get_last_version_by_subset_name @@ -31,12 +31,14 @@ class CollectRender(pyblish.api.InstancePlugin): files_by_aov = RenderProducts().get_beauty(instance.name) folder = folder.replace("\\", "/") - aovs = RenderProducts().get_aovs(instance.name) + aovs = RenderProducts().get_aovs(instance.name, instance) files_by_aov.update(aovs) if "expectedFiles" not in instance.data: instance.data["expectedFiles"] = list() + instance.data["files"] = list() instance.data["expectedFiles"].append(files_by_aov) + instance.data["files"].append(files_by_aov) img_format = RenderProducts().image_format() project_name = context.data["projectName"] @@ -62,6 +64,8 @@ class CollectRender(pyblish.api.InstancePlugin): instance.data["renderProducts"] = colorspace.ARenderProduct() instance.data["publishJobState"] = "Suspended" instance.data["attachTo"] = [] + renderer_class = get_current_renderer() + renderer = str(renderer_class).split(":")[0] # also need to get the render dir for coversion data = { "asset": asset, @@ -71,6 +75,7 @@ class CollectRender(pyblish.api.InstancePlugin): "imageFormat": img_format, "family": 'maxrender', "families": ['maxrender'], + "renderer": renderer, "source": filepath, "plugin": "3dsmax", "frameStart": int(rt.rendStart), diff --git a/openpype/hosts/max/plugins/publish/save_scene.py b/openpype/hosts/max/plugins/publish/save_scene.py index 93d97a3de5..a40788ab41 100644 --- a/openpype/hosts/max/plugins/publish/save_scene.py +++ b/openpype/hosts/max/plugins/publish/save_scene.py @@ -18,9 +18,4 @@ class SaveCurrentScene(pyblish.api.ContextPlugin): file = rt.maxFileName current = os.path.join(folder, file) assert context.data["currentFile"] == current - - if rt.checkForSave(): - self.log.debug("Skipping file save as there " - "are no modifications..") - return rt.saveMaxFile(current) diff --git a/openpype/hosts/max/plugins/publish/validate_deadline_publish.py b/openpype/hosts/max/plugins/publish/validate_deadline_publish.py new file mode 100644 index 0000000000..f516e09337 --- /dev/null +++ b/openpype/hosts/max/plugins/publish/validate_deadline_publish.py @@ -0,0 +1,41 @@ +import os +import pyblish.api +from pymxs import runtime as rt +from openpype.pipeline.publish import ( + RepairAction, + ValidateContentsOrder, + PublishValidationError, + OptionalPyblishPluginMixin +) +from openpype.hosts.max.api.lib_rendersettings import RenderSettings + + +class ValidateDeadlinePublish(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): + """Validates Render File Directory is + not the same in every submission + """ + + order = ValidateContentsOrder + families = ["maxrender"] + hosts = ["max"] + label = "Render Output for Deadline" + optional = True + actions = [RepairAction] + + def process(self, instance): + if not self.is_active(instance.data): + return + file = rt.maxFileName + filename, ext = os.path.splitext(file) + if filename not in rt.rendOutputFilename: + raise PublishValidationError( + "Directory of RenderOutput doesn't " + "have with the current Max SceneName " + "please repair to rename the folder!" + ) + + @classmethod + def repair(cls, instance): + container= instance.data.get("instance_node") + RenderSettings().render_output(container) diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index 3fde667dfe..d7ba7107a3 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -133,6 +133,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, # Add list of expected files to job # --------------------------------- exp = instance.data.get("expectedFiles") + for filepath in self._iter_expected_files(exp): job_info.OutputDirectory += os.path.dirname(filepath) job_info.OutputFilename += os.path.basename(filepath) @@ -204,8 +205,6 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, old_output_dir = os.path.dirname(first_file) output_beauty = RenderSettings().get_render_output(instance.name, old_output_dir) - files = instance.data["expectedFiles"] - first_file = next(self._iter_expected_files(files)) rgb_bname = os.path.basename(output_beauty) dir = os.path.dirname(first_file) beauty_name = f"{dir}/{rgb_bname}" @@ -231,6 +230,9 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, new_elem = new_elem.replace("/", "\\") plugin_data["RenderElementOutputFilename%d" % i] = new_elem # noqa + if renderer == "Redshift_Renderer": + plugin_data["redshift_SeparateAovFiles"] = instance.data.get( + "separateAovFiles", False) self.log.debug("plugin data:{}".format(plugin_data)) plugin_info.update(plugin_data) @@ -239,72 +241,10 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, def from_published_scene(self, replace_in_path=True): instance = self._instance - workfile_instance = self._get_workfile_instance(instance.context) - if workfile_instance is None: - return - - # determine published path from Anatomy. - template_data = workfile_instance.data.get("anatomyData") - rep = workfile_instance.data["representations"][0] - template_data["representation"] = rep.get("name") - template_data["ext"] = rep.get("ext") - template_data["comment"] = None - - anatomy = instance.context.data['anatomy'] - template_obj = anatomy.templates_obj["publish"]["path"] - template_filled = template_obj.format_strict(template_data) - file_path = os.path.normpath(template_filled) - - self.log.info("Using published scene for render {}".format(file_path)) - - if not os.path.exists(file_path): - self.log.error("published scene does not exist!") - raise - - if not replace_in_path: - return file_path - - # now we need to switch scene in expected files - # because token will now point to published - # scene file and that might differ from current one - def _clean_name(path): - return os.path.splitext(os.path.basename(path))[0] - - new_scene = _clean_name(file_path) - orig_scene = _clean_name(instance.data["AssetName"]) - expected_files = instance.data.get("expectedFiles") - - if isinstance(expected_files[0], dict): - # we have aovs and we need to iterate over them - new_exp = {} - for aov, files in expected_files[0].items(): - replaced_files = [] - for f in files: - replaced_files.append( - str(f).replace(orig_scene, new_scene) - ) - new_exp[aov] = replaced_files - # [] might be too much here, TODO - instance.data["expectedFiles"] = [new_exp] - else: - new_exp = [] - for f in expected_files: - new_exp.append( - str(f).replace(orig_scene, new_scene) - ) - instance.data["expectedFiles"] = new_exp - - metadata_folder = instance.data.get("publishRenderMetadataFolder") - if metadata_folder: - metadata_folder = metadata_folder.replace(orig_scene, - new_scene) - instance.data["publishRenderMetadataFolder"] = metadata_folder - - self.log.info("Scene name was switched {} -> {}".format( - orig_scene, new_scene - )) - - return file_path + if instance.data["renderer"] == "Redshift_Renderer": + self.log.debug("Using Redshift...published scene wont be used..") + replace_in_path = False + return replace_in_path @staticmethod def _iter_expected_files(exp): From dbb175204adf16e763d2c2295d6ad675988453ba Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 19:29:35 +0800 Subject: [PATCH 104/147] hound --- openpype/hosts/max/plugins/publish/validate_deadline_publish.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/validate_deadline_publish.py b/openpype/hosts/max/plugins/publish/validate_deadline_publish.py index f516e09337..c5bc979043 100644 --- a/openpype/hosts/max/plugins/publish/validate_deadline_publish.py +++ b/openpype/hosts/max/plugins/publish/validate_deadline_publish.py @@ -37,5 +37,5 @@ class ValidateDeadlinePublish(pyblish.api.InstancePlugin, @classmethod def repair(cls, instance): - container= instance.data.get("instance_node") + container = instance.data.get("instance_node") RenderSettings().render_output(container) From eb5b5bf492c69d3369d944ff019f36a6e306b3bd Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 29 May 2023 12:59:54 +0100 Subject: [PATCH 105/147] Set back sequencer and viewport when updating layout or camera --- .../hosts/unreal/plugins/load/load_camera.py | 23 ++++++++++++-- .../hosts/unreal/plugins/load/load_layout.py | 31 ++++++++++++++----- 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 3ed7b055e3..59ea14697d 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -3,9 +3,12 @@ from pathlib import Path import unreal -from unreal import EditorAssetLibrary -from unreal import EditorLevelLibrary -from unreal import EditorLevelUtils +from unreal import ( + EditorAssetLibrary, + EditorLevelLibrary, + EditorLevelUtils, + LevelSequenceEditorBlueprintLibrary as LevelSequenceLib, +) from openpype.client import get_asset_by_name from openpype.pipeline import ( AYON_CONTAINER_ID, @@ -259,6 +262,13 @@ class CameraLoader(plugin.Loader): def update(self, container, representation): ar = unreal.AssetRegistryHelpers.get_asset_registry() + curr_level_sequence = LevelSequenceLib.get_current_level_sequence() + curr_time = LevelSequenceLib.get_current_time() + is_cam_lock = LevelSequenceLib.is_camera_cut_locked_to_viewport() + + editor_subsystem = unreal.UnrealEditorSubsystem() + vp_loc, vp_rot = editor_subsystem.get_level_viewport_camera_info() + asset_dir = container.get('namespace') EditorLevelLibrary.save_current_level() @@ -416,6 +426,13 @@ class CameraLoader(plugin.Loader): EditorLevelLibrary.load_level(master_level) + if curr_level_sequence: + LevelSequenceLib.open_level_sequence(curr_level_sequence) + LevelSequenceLib.set_current_time(curr_time) + LevelSequenceLib.set_lock_camera_cut_to_viewport(is_cam_lock) + + editor_subsystem.set_level_viewport_camera_info(vp_loc, vp_rot) + def remove(self, container): asset_dir = container.get('namespace') path = Path(asset_dir) diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index 51ca0383e0..86b2e1456c 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -5,13 +5,16 @@ import collections from pathlib import Path import unreal -from unreal import EditorAssetLibrary -from unreal import EditorLevelLibrary -from unreal import EditorLevelUtils -from unreal import AssetToolsHelpers -from unreal import FBXImportType -from unreal import MovieSceneLevelVisibilityTrack -from unreal import MovieSceneSubTrack +from unreal import ( + EditorAssetLibrary, + EditorLevelLibrary, + EditorLevelUtils, + AssetToolsHelpers, + FBXImportType, + MovieSceneLevelVisibilityTrack, + MovieSceneSubTrack, + LevelSequenceEditorBlueprintLibrary as LevelSequenceLib, +) from openpype.client import get_asset_by_name, get_representations from openpype.pipeline import ( @@ -661,6 +664,13 @@ class LayoutLoader(plugin.Loader): ar = unreal.AssetRegistryHelpers.get_asset_registry() + curr_level_sequence = LevelSequenceLib.get_current_level_sequence() + curr_time = LevelSequenceLib.get_current_time() + is_cam_lock = LevelSequenceLib.is_camera_cut_locked_to_viewport() + + editor_subsystem = unreal.UnrealEditorSubsystem() + vp_loc, vp_rot = editor_subsystem.get_level_viewport_camera_info() + root = "/Game/Ayon" asset_dir = container.get('namespace') @@ -742,6 +752,13 @@ class LayoutLoader(plugin.Loader): elif prev_level: EditorLevelLibrary.load_level(prev_level) + if curr_level_sequence: + LevelSequenceLib.open_level_sequence(curr_level_sequence) + LevelSequenceLib.set_current_time(curr_time) + LevelSequenceLib.set_lock_camera_cut_to_viewport(is_cam_lock) + + editor_subsystem.set_level_viewport_camera_info(vp_loc, vp_rot) + def remove(self, container): """ Delete the layout. First, check if the assets loaded with the layout From e506d88ed3d113f608759e883dcdf17dcb6f5ccc Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 20:33:40 +0800 Subject: [PATCH 106/147] style fix --- openpype/hosts/max/plugins/create/create_render.py | 4 +--- .../modules/deadline/plugins/publish/submit_max_deadline.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index 3d5ed781a4..6c581cdcf4 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -47,10 +47,8 @@ class CreateRender(plugin.MaxCreator): def get_pre_create_attr_defs(self): attrs = super(CreateRender, self).get_pre_create_attr_defs() - return attrs + [ BoolDef("separateAovFiles", label="Separate Aov Files", - default=False), - + default=False) ] diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index d7ba7107a3..b6a30e36b7 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -232,7 +232,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, if renderer == "Redshift_Renderer": plugin_data["redshift_SeparateAovFiles"] = instance.data.get( - "separateAovFiles", False) + "separateAovFiles") self.log.debug("plugin data:{}".format(plugin_data)) plugin_info.update(plugin_data) From 5f763a4e8e65d0aeb8e4515e69ec768f6f2c5f4d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 20:36:38 +0800 Subject: [PATCH 107/147] dont add separate aov as instance data --- openpype/hosts/max/api/lib_renderproducts.py | 4 ++-- openpype/hosts/max/plugins/create/create_render.py | 10 ---------- openpype/hosts/max/plugins/publish/collect_render.py | 2 +- 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 7e016f6f15..a6427bf7c5 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -38,7 +38,7 @@ class RenderProducts(object): } return render_dict - def get_aovs(self, container, instance): + def get_aovs(self, container): render_dir = os.path.dirname(rt.rendOutputFilename) output_file = os.path.join(render_dir, @@ -71,7 +71,7 @@ class RenderProducts(object): if renderer == "Redshift_Renderer": render_name = self.get_render_elements_name() if render_name: - rs_AovFiles = instance.data.get("separateAovFiles") + rs_AovFiles = rt.RedShift_Renderer().separateAovFiles if img_fmt == "exr" and not rs_AovFiles: for name in render_name: if name == "RsCryptomatte": diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index 6c581cdcf4..be5dece05b 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -19,8 +19,6 @@ class CreateRender(plugin.MaxCreator): file = rt.maxFileName filename, _ = os.path.splitext(file) instance_data["AssetName"] = filename - instance_data["separateAovFiles"] = ( - pre_create_data.get("separateAovFiles")) instance = super(CreateRender, self).create( subset_name, @@ -44,11 +42,3 @@ class CreateRender(plugin.MaxCreator): RenderSettings().set_render_camera(sel_obj) # set output paths for rendering(mandatory for deadline) RenderSettings().render_output(container_name) - - def get_pre_create_attr_defs(self): - attrs = super(CreateRender, self).get_pre_create_attr_defs() - return attrs + [ - BoolDef("separateAovFiles", - label="Separate Aov Files", - default=False) - ] diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 14d23c42fc..77ab5f654d 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -31,7 +31,7 @@ class CollectRender(pyblish.api.InstancePlugin): files_by_aov = RenderProducts().get_beauty(instance.name) folder = folder.replace("\\", "/") - aovs = RenderProducts().get_aovs(instance.name, instance) + aovs = RenderProducts().get_aovs(instance.name) files_by_aov.update(aovs) if "expectedFiles" not in instance.data: From 982186ffa08618f8d590e894356595f886af0f57 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 20:38:17 +0800 Subject: [PATCH 108/147] remove unused import --- openpype/hosts/max/plugins/create/create_render.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index be5dece05b..5ad895b86e 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -2,7 +2,6 @@ """Creator plugin for creating camera.""" import os from openpype.hosts.max.api import plugin -from openpype.lib import BoolDef from openpype.pipeline import CreatedInstance from openpype.hosts.max.api.lib_rendersettings import RenderSettings From bafc085ec19e624c163d6a862b9fcc6e0edab983 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 22:56:47 +0800 Subject: [PATCH 109/147] updating validator's comment --- .../max/plugins/publish/validate_deadline_publish.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_deadline_publish.py b/openpype/hosts/max/plugins/publish/validate_deadline_publish.py index c5bc979043..b2f0e863f4 100644 --- a/openpype/hosts/max/plugins/publish/validate_deadline_publish.py +++ b/openpype/hosts/max/plugins/publish/validate_deadline_publish.py @@ -30,12 +30,14 @@ class ValidateDeadlinePublish(pyblish.api.InstancePlugin, filename, ext = os.path.splitext(file) if filename not in rt.rendOutputFilename: raise PublishValidationError( - "Directory of RenderOutput doesn't " - "have with the current Max SceneName " - "please repair to rename the folder!" + "Render output folder " + "doesn't match the max scene name! " + "Use Repair action to " + "fix the folder file path.." ) @classmethod def repair(cls, instance): container = instance.data.get("instance_node") RenderSettings().render_output(container) + cls.log.debug("Reset the render output folder...") From a4e9eaf3c38aaa4bb31784067d35d54656ae8333 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Mon, 29 May 2023 23:24:48 +0200 Subject: [PATCH 110/147] Update openpype/hosts/max/plugins/load/load_redshift_proxy.py Co-authored-by: Roy Nieterau --- openpype/hosts/max/plugins/load/load_redshift_proxy.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/max/plugins/load/load_redshift_proxy.py b/openpype/hosts/max/plugins/load/load_redshift_proxy.py index 9451e5299b..31692f6367 100644 --- a/openpype/hosts/max/plugins/load/load_redshift_proxy.py +++ b/openpype/hosts/max/plugins/load/load_redshift_proxy.py @@ -10,7 +10,6 @@ from openpype.hosts.max.api import lib class RedshiftProxyLoader(load.LoaderPlugin): - """Load rs files with Redshift Proxy""" label = "Load Redshift Proxy" From 279b3dc767fc870a8632abc625f1e2dbf3706add Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 30 May 2023 12:09:53 +0200 Subject: [PATCH 111/147] Set explicit startup script path --- .../resolve/hooks/pre_resolve_launch_last_workfile.py | 11 +++++++++++ .../utility_scripts/openpype_startup.scriptlib | 7 +++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/resolve/hooks/pre_resolve_launch_last_workfile.py b/openpype/hosts/resolve/hooks/pre_resolve_launch_last_workfile.py index 6db3cc28b2..2ad4352b82 100644 --- a/openpype/hosts/resolve/hooks/pre_resolve_launch_last_workfile.py +++ b/openpype/hosts/resolve/hooks/pre_resolve_launch_last_workfile.py @@ -1,6 +1,7 @@ import os from openpype.lib import PreLaunchHook +import openpype.hosts.resolve class ResolveLaunchLastWorkfile(PreLaunchHook): @@ -33,3 +34,13 @@ class ResolveLaunchLastWorkfile(PreLaunchHook): f"last workfile: {last_workfile}") key = "OPENPYPE_RESOLVE_OPEN_ON_LAUNCH" self.launch_context.env[key] = last_workfile + + # Set the openpype prelaunch startup script path for easy access + # in the LUA .scriptlib code + op_resolve_root = os.path.dirname(openpype.hosts.resolve.__file__) + script_path = os.path.join(op_resolve_root, "startup.py") + key = "OPENPYPE_RESOLVE_STARTUP_SCRIPT" + self.launch_context.env[key] = script_path + self.log.info("Setting OPENPYPE_RESOLVE_STARTUP_SCRIPT to: " + f"{script_path}") + diff --git a/openpype/hosts/resolve/utility_scripts/openpype_startup.scriptlib b/openpype/hosts/resolve/utility_scripts/openpype_startup.scriptlib index 9fca666d78..ec9b30a18d 100644 --- a/openpype/hosts/resolve/utility_scripts/openpype_startup.scriptlib +++ b/openpype/hosts/resolve/utility_scripts/openpype_startup.scriptlib @@ -5,10 +5,9 @@ function file_exists(name) end -openpype_root = os.getenv("OPENPYPE_ROOT") -if openpype_root ~= nil then - script = openpype_root .. "/openpype/hosts/resolve/startup.py" - script = fusion:MapPath(script) +openpype_startup_script = os.getenv("OPENPYPE_RESOLVE_STARTUP_SCRIPT") +if openpype_startup_script ~= nil then + script = fusion:MapPath(openpype_startup_script) if file_exists(script) then -- We must use RunScript to ensure it runs in a separate From 990737623bcfab7c5f7502e3cb4a0f2156f0891c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 30 May 2023 12:20:17 +0200 Subject: [PATCH 112/147] Cosmetics --- openpype/hosts/resolve/hooks/pre_resolve_launch_last_workfile.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/resolve/hooks/pre_resolve_launch_last_workfile.py b/openpype/hosts/resolve/hooks/pre_resolve_launch_last_workfile.py index 2ad4352b82..0e27ddb8c3 100644 --- a/openpype/hosts/resolve/hooks/pre_resolve_launch_last_workfile.py +++ b/openpype/hosts/resolve/hooks/pre_resolve_launch_last_workfile.py @@ -43,4 +43,3 @@ class ResolveLaunchLastWorkfile(PreLaunchHook): self.launch_context.env[key] = script_path self.log.info("Setting OPENPYPE_RESOLVE_STARTUP_SCRIPT to: " f"{script_path}") - From 307eca8c28aeb1afeb68ba1e93e63e993c21a084 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 30 May 2023 13:24:17 +0200 Subject: [PATCH 113/147] Cleanup Resolve startup script + add setting for launch menu on start --- openpype/hosts/resolve/startup.py | 46 ++++++++++++++----- .../defaults/project_settings/resolve.json | 1 + .../schema_project_resolve.json | 5 ++ 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/resolve/startup.py b/openpype/hosts/resolve/startup.py index 4aeb106ef1..79a64e0fbf 100644 --- a/openpype/hosts/resolve/startup.py +++ b/openpype/hosts/resolve/startup.py @@ -1,26 +1,44 @@ +"""This script is used as a startup script in Resolve through a .scriptlib file + +It triggers directly after the launch of Resolve and it's recommended to keep +it optimized for fast performance since the Resolve UI is actually interactive +while this is running. As such, there's nothing ensuring the user isn't +continuing manually before any of the logic here runs. As such we also try +to delay any imports as much as possible. + +This code runs in a separate process to the main Resolve process. + +""" import os -# Importing this takes a little over a second and thus this means -# that we have about 1.5 seconds delay before the workfile will actually -# be opened at the minimum import openpype.hosts.resolve.api -def launch_menu(): - from openpype.pipeline import install_host - print("Launching Resolve OpenPype menu..") +def ensure_installed_host(): + """Install resolve host with openpype and return the registered host. + + This function can be called multiple times without triggering an + additional install. + """ + from openpype.pipeline import install_host, registered_host + host = registered_host() + if host: + return host - # Activate resolve from openpype install_host(openpype.hosts.resolve.api) + return registered_host() + +def launch_menu(): + print("Launching Resolve OpenPype menu..") + ensure_installed_host() openpype.hosts.resolve.api.launch_pype_menu() def open_file(path): # Avoid the need to "install" the host - openpype.hosts.resolve.api.bmdvr = resolve # noqa - openpype.hosts.resolve.api.bmdvf = fusion # noqa - openpype.hosts.resolve.api.open_file(path) + host = ensure_installed_host() + host.open_file(path) def main(): @@ -32,8 +50,12 @@ def main(): print("No last workfile set to open. Skipping..") # Launch OpenPype menu - # TODO: Add a setting to enable/disable this - launch_menu() + from openpype.settings import get_project_settings + from openpype.pipeline.context_tools import get_current_project_name + project_name = get_current_project_name() + settings = get_project_settings(project_name) + if settings.get("resolve", {}).get("launch_openpype_menu_on_start", True): + launch_menu() if __name__ == "__main__": diff --git a/openpype/settings/defaults/project_settings/resolve.json b/openpype/settings/defaults/project_settings/resolve.json index 264f3bd902..56efa78e89 100644 --- a/openpype/settings/defaults/project_settings/resolve.json +++ b/openpype/settings/defaults/project_settings/resolve.json @@ -1,4 +1,5 @@ { + "launch_openpype_menu_on_start": false, "imageio": { "ocio_config": { "enabled": false, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_resolve.json b/openpype/settings/entities/schemas/projects_schema/schema_project_resolve.json index b326f22394..6f98bdd3bd 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_resolve.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_resolve.json @@ -5,6 +5,11 @@ "label": "DaVinci Resolve", "is_file": true, "children": [ + { + "type": "boolean", + "key": "launch_openpype_menu_on_start", + "label": "Launch OpenPype menu on start of Resolve" + }, { "key": "imageio", "type": "dict", From 5fbae39a745b0016b2ada9d054b696aef2007fee Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 30 May 2023 13:32:27 +0200 Subject: [PATCH 114/147] Ftrack: Role names are not case sensitive in ftrack event server status action (#5058) * statuser is not case sensitive about role names * safer role check --- .../ftrack/lib/ftrack_action_handler.py | 23 +++++++++++++++---- .../ftrack/scripts/sub_event_status.py | 4 ++-- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/openpype/modules/ftrack/lib/ftrack_action_handler.py b/openpype/modules/ftrack/lib/ftrack_action_handler.py index 07b3a780a2..1be4353b26 100644 --- a/openpype/modules/ftrack/lib/ftrack_action_handler.py +++ b/openpype/modules/ftrack/lib/ftrack_action_handler.py @@ -234,6 +234,10 @@ class BaseAction(BaseHandler): if not settings_roles: return default + user_roles = { + role_name.lower() + for role_name in user_roles + } for role_name in settings_roles: if role_name.lower() in user_roles: return True @@ -264,8 +268,15 @@ class BaseAction(BaseHandler): return user_entity @classmethod - def get_user_roles_from_event(cls, session, event): - """Query user entity from event.""" + def get_user_roles_from_event(cls, session, event, lower=True): + """Get user roles based on data in event. + + Args: + session (ftrack_api.Session): Prepared ftrack session. + event (ftrack_api.event.Event): Event which is processed. + lower (Optional[bool]): Lower the role names. Default 'True'. + """ + not_set = object() user_roles = event["data"].get("user_roles", not_set) @@ -273,7 +284,10 @@ class BaseAction(BaseHandler): user_roles = [] user_entity = cls.get_user_entity_from_event(session, event) for role in user_entity["user_security_roles"]: - user_roles.append(role["security_role"]["name"].lower()) + role_name = role["security_role"]["name"] + if lower: + role_name = role_name.lower() + user_roles.append(role_name) event["data"]["user_roles"] = user_roles return user_roles @@ -322,7 +336,8 @@ class BaseAction(BaseHandler): if not settings.get(self.settings_enabled_key, True): return False - user_role_list = self.get_user_roles_from_event(session, event) + user_role_list = self.get_user_roles_from_event( + session, event, lower=False) if not self.roles_check(settings.get("role_list"), user_role_list): return False return True diff --git a/openpype/modules/ftrack/scripts/sub_event_status.py b/openpype/modules/ftrack/scripts/sub_event_status.py index dc5836e7f2..c6c2e9e1f6 100644 --- a/openpype/modules/ftrack/scripts/sub_event_status.py +++ b/openpype/modules/ftrack/scripts/sub_event_status.py @@ -296,9 +296,9 @@ def server_activity_validate_user(event): if not user_ent: return False - role_list = ["Pypeclub", "Administrator"] + role_list = {"pypeclub", "administrator"} for role in user_ent["user_security_roles"]: - if role["security_role"]["name"] in role_list: + if role["security_role"]["name"].lower() in role_list: return True return False From 4bc61d1c89f1c3adfbbc7ba80fa2fff5f156eec0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 30 May 2023 13:33:58 +0200 Subject: [PATCH 115/147] Fix border widget --- .../publisher/widgets/border_label_widget.py | 125 +++++++++++++----- 1 file changed, 94 insertions(+), 31 deletions(-) diff --git a/openpype/tools/publisher/widgets/border_label_widget.py b/openpype/tools/publisher/widgets/border_label_widget.py index 5617e159cd..1381d74eb3 100644 --- a/openpype/tools/publisher/widgets/border_label_widget.py +++ b/openpype/tools/publisher/widgets/border_label_widget.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- +from math import ceil from qtpy import QtWidgets, QtCore, QtGui from openpype.style import get_objected_colors @@ -14,32 +15,44 @@ class _VLineWidget(QtWidgets.QWidget): It is expected that parent widget will set width. """ - def __init__(self, color, left, parent): + def __init__(self, color, line_size, left, parent): super(_VLineWidget, self).__init__(parent) self._color = color self._left = left + self._line_size = line_size + + def set_line_size(self, line_size): + self._line_size = line_size def paintEvent(self, event): if not self.isVisible(): return - if self._left: - pos_x = 0 - else: - pos_x = self.width() + pos_x = self._line_size * 0.5 + if not self._left: + pos_x = self.width() - pos_x + painter = QtGui.QPainter(self) painter.setRenderHints( QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform ) + if self._color: pen = QtGui.QPen(self._color) else: pen = painter.pen() - pen.setWidth(1) + pen.setWidth(self._line_size) painter.setPen(pen) painter.setBrush(QtCore.Qt.transparent) - painter.drawLine(pos_x, 0, pos_x, self.height()) + painter.drawRect( + QtCore.QRectF( + pos_x, + -self._line_size, + pos_x + (self.width() * 2), + self.height() + (self._line_size * 2) + ) + ) painter.end() @@ -56,34 +69,46 @@ class _HBottomLineWidget(QtWidgets.QWidget): It is expected that parent widget will set height and radius. """ - def __init__(self, color, parent): + def __init__(self, color, line_size, parent): super(_HBottomLineWidget, self).__init__(parent) self._color = color self._radius = 0 + self._line_size = line_size def set_radius(self, radius): self._radius = radius + def set_line_size(self, line_size): + self._line_size = line_size + def paintEvent(self, event): if not self.isVisible(): return - rect = QtCore.QRect( - 0, -self._radius, self.width(), self.height() + self._radius + x_offset = self._line_size * 0.5 + rect = QtCore.QRectF( + x_offset, + -self._radius, + self.width() - (2 * x_offset), + (self.height() + self._radius) - x_offset ) painter = QtGui.QPainter(self) painter.setRenderHints( QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform ) + if self._color: pen = QtGui.QPen(self._color) else: pen = painter.pen() - pen.setWidth(1) + pen.setWidth(self._line_size) painter.setPen(pen) painter.setBrush(QtCore.Qt.transparent) - painter.drawRoundedRect(rect, self._radius, self._radius) + if self._radius: + painter.drawRoundedRect(rect, self._radius, self._radius) + else: + painter.drawRect(rect) painter.end() @@ -102,30 +127,38 @@ class _HTopCornerLineWidget(QtWidgets.QWidget): It is expected that parent widget will set height and radius. """ - def __init__(self, color, left_side, parent): + + def __init__(self, color, line_size, left_side, parent): super(_HTopCornerLineWidget, self).__init__(parent) self._left_side = left_side + self._line_size = line_size self._color = color self._radius = 0 def set_radius(self, radius): self._radius = radius + def set_line_size(self, line_size): + self._line_size = line_size + def paintEvent(self, event): if not self.isVisible(): return - pos_y = self.height() / 2 - + pos_y = self.height() * 0.5 + x_offset = self._line_size * 0.5 if self._left_side: - rect = QtCore.QRect( - 0, pos_y, self.width() + self._radius, self.height() + rect = QtCore.QRectF( + x_offset, + pos_y, + self.width() + self._radius + x_offset, + self.height() ) else: - rect = QtCore.QRect( - -self._radius, + rect = QtCore.QRectF( + (-self._radius), pos_y, - self.width() + self._radius, + (self.width() + self._radius) - x_offset, self.height() ) @@ -138,10 +171,13 @@ class _HTopCornerLineWidget(QtWidgets.QWidget): pen = QtGui.QPen(self._color) else: pen = painter.pen() - pen.setWidth(1) + pen.setWidth(self._line_size) painter.setPen(pen) painter.setBrush(QtCore.Qt.transparent) - painter.drawRoundedRect(rect, self._radius, self._radius) + if self._radius: + painter.drawRoundedRect(rect, self._radius, self._radius) + else: + painter.drawRect(rect) painter.end() @@ -163,8 +199,10 @@ class BorderedLabelWidget(QtWidgets.QFrame): if color_value: color = color_value.get_qcolor() - top_left_w = _HTopCornerLineWidget(color, True, self) - top_right_w = _HTopCornerLineWidget(color, False, self) + line_size = 1 + + top_left_w = _HTopCornerLineWidget(color, line_size, True, self) + top_right_w = _HTopCornerLineWidget(color, line_size, False, self) label_widget = QtWidgets.QLabel(label, self) @@ -175,10 +213,10 @@ class BorderedLabelWidget(QtWidgets.QFrame): top_layout.addWidget(label_widget, 0) top_layout.addWidget(top_right_w, 1) - left_w = _VLineWidget(color, True, self) - right_w = _VLineWidget(color, False, self) + left_w = _VLineWidget(color, line_size, True, self) + right_w = _VLineWidget(color, line_size, False, self) - bottom_w = _HBottomLineWidget(color, self) + bottom_w = _HBottomLineWidget(color, line_size, self) center_layout = QtWidgets.QHBoxLayout() center_layout.setContentsMargins(5, 5, 5, 5) @@ -201,6 +239,7 @@ class BorderedLabelWidget(QtWidgets.QFrame): self._widget = None self._radius = 0 + self._line_size = line_size self._top_left_w = top_left_w self._top_right_w = top_right_w @@ -216,14 +255,38 @@ class BorderedLabelWidget(QtWidgets.QFrame): value, value, value, value ) + def set_line_size(self, line_size): + if self._line_size == line_size: + return + self._line_size = line_size + for widget in ( + self._top_left_w, + self._top_right_w, + self._left_w, + self._right_w, + self._bottom_w + ): + widget.set_line_size(line_size) + self._recalculate_sizes() + def showEvent(self, event): super(BorderedLabelWidget, self).showEvent(event) + self._recalculate_sizes() + def _recalculate_sizes(self): height = self._label_widget.height() - radius = (height + (height % 2)) / 2 + radius = int((height + (height % 2)) / 2) self._radius = radius - side_width = 1 + radius + radius_size = self._line_size + 1 + if radius_size < radius: + radius_size = radius + + if radius: + side_width = self._line_size + radius + else: + side_width = self._line_size + 1 + # Don't use fixed width/height as that would set also set # the other size (When fixed width is set then is also set # fixed height). @@ -231,8 +294,8 @@ class BorderedLabelWidget(QtWidgets.QFrame): self._left_w.setMaximumWidth(side_width) self._right_w.setMinimumWidth(side_width) self._right_w.setMaximumWidth(side_width) - self._bottom_w.setMinimumHeight(radius) - self._bottom_w.setMaximumHeight(radius) + self._bottom_w.setMinimumHeight(radius_size) + self._bottom_w.setMaximumHeight(radius_size) self._bottom_w.set_radius(radius) self._top_right_w.set_radius(radius) self._top_left_w.set_radius(radius) From 26b99db61e4ab17033bfaa5b0f9099a3b6561275 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 30 May 2023 15:18:53 +0200 Subject: [PATCH 116/147] removed unused import --- openpype/tools/publisher/widgets/border_label_widget.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/tools/publisher/widgets/border_label_widget.py b/openpype/tools/publisher/widgets/border_label_widget.py index 1381d74eb3..e5693368b1 100644 --- a/openpype/tools/publisher/widgets/border_label_widget.py +++ b/openpype/tools/publisher/widgets/border_label_widget.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- -from math import ceil from qtpy import QtWidgets, QtCore, QtGui from openpype.style import get_objected_colors From 2e58cd2e1a1a1dd23e0bc59bc1cbc414fa6d1ec9 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 30 May 2023 21:44:26 +0800 Subject: [PATCH 117/147] move the custom popup menu to nuke/startup and add the frame setting to Openpype tool menu --- openpype/hosts/nuke/api/lib.py | 32 --------- openpype/hosts/nuke/api/pipeline.py | 5 -- .../nuke/startup}/custom_popup.py | 71 ++++++++++++++----- .../defaults/project_settings/nuke.json | 7 ++ 4 files changed, 61 insertions(+), 54 deletions(-) rename openpype/{widgets => hosts/nuke/startup}/custom_popup.py (65%) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 94a0ff15ad..59a63d1373 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2358,38 +2358,6 @@ class WorkfileSettings(object): # add colorspace menu item self.set_colorspace() - def reset_frame_range_read_nodes(self): - from openpype.widgets import custom_popup - parent = get_main_window() - dialog = custom_popup.CustomScriptDialog(parent=parent) - dialog.setWindowTitle("Frame Range") - dialog.set_name("Frame Range: ") - dialog.set_line_edit("%s - %s" % (nuke.root().firstFrame(), - nuke.root().lastFrame())) - frame = dialog.widgets["line_edit"] - selection = dialog.widgets["selection"] - dialog.on_clicked.connect( - lambda: set_frame_range(frame, selection) - ) - - def set_frame_range(frame, selection): - frame_range = frame.text() - selected = selection.isChecked() - if not nuke.allNodes("Read"): - return - for read_node in nuke.allNodes("Read"): - if selected: - if not nuke.selectedNodes(): - return - if read_node in nuke.selectedNodes(): - read_node["frame_mode"].setValue("start_at") - read_node["frame"].setValue(frame_range) - else: - read_node["frame_mode"].setValue("start_at") - read_node["frame"].setValue(frame_range) - dialog.show() - - return False def set_favorites(self): from .utils import set_context_favorites diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index 33e25d3c81..75b0f80d21 100644 --- a/openpype/hosts/nuke/api/pipeline.py +++ b/openpype/hosts/nuke/api/pipeline.py @@ -286,11 +286,6 @@ def _install_menu(): lambda: WorkfileSettings().set_context_settings() ) - menu.addSeparator() - menu.addCommand( - "Set Frame Range(Read Node)", - lambda: WorkfileSettings().reset_frame_range_read_nodes() - ) menu.addSeparator() menu.addCommand( "Build Workfile", diff --git a/openpype/widgets/custom_popup.py b/openpype/hosts/nuke/startup/custom_popup.py similarity index 65% rename from openpype/widgets/custom_popup.py rename to openpype/hosts/nuke/startup/custom_popup.py index be4b0c32d5..c85577133c 100644 --- a/openpype/widgets/custom_popup.py +++ b/openpype/hosts/nuke/startup/custom_popup.py @@ -1,9 +1,26 @@ import sys import contextlib - +import re +import nuke from PySide2 import QtCore, QtWidgets +def get_main_window(): + """Acquire Nuke's main window""" + main_window = None + if main_window is None: + + top_widgets = QtWidgets.QApplication.topLevelWidgets() + name = "Foundry::UI::DockMainWindow" + for widget in top_widgets: + if ( + widget.inherits("QMainWindow") + and widget.metaObject().className() == name + ): + main_window = widget + break + return main_window + class CustomScriptDialog(QtWidgets.QDialog): """A Popup that moves itself to bottom right of screen on show event. @@ -14,6 +31,9 @@ class CustomScriptDialog(QtWidgets.QDialog): on_clicked = QtCore.Signal() on_line_changed = QtCore.Signal(str) + context = None + + def __init__(self, parent=None, *args, **kwargs): super(CustomScriptDialog, self).__init__(parent=parent, @@ -23,23 +43,25 @@ class CustomScriptDialog(QtWidgets.QDialog): # Layout layout = QtWidgets.QVBoxLayout(self) - line_layout = QtWidgets.QHBoxLayout() - line_layout.setContentsMargins(10, 5, 10, 10) + frame_layout = QtWidgets.QHBoxLayout() + frame_layout.setContentsMargins(10, 5, 10, 10) selection_layout = QtWidgets.QHBoxLayout() selection_layout.setContentsMargins(10, 5, 10, 10) button_layout = QtWidgets.QHBoxLayout() button_layout.setContentsMargins(10, 5, 10, 10) # Increase spacing slightly for readability - line_layout.setSpacing(10) + frame_layout.setSpacing(10) button_layout.setSpacing(10) - name = QtWidgets.QLabel("") + name = QtWidgets.QLabel("Frame Range: ") name.setStyleSheet(""" QLabel { font-size: 12px; } """) - line_edit = QtWidgets.QLineEdit("") + line_edit = QtWidgets.QLineEdit( + "%s-%s" % (nuke.root().firstFrame(), + nuke.root().lastFrame())) selection_name = QtWidgets.QLabel("Use Selection") selection_name.setStyleSheet(""" QLabel { @@ -54,13 +76,13 @@ class CustomScriptDialog(QtWidgets.QDialog): cancel.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum) - line_layout.addWidget(name) - line_layout.addWidget(line_edit) + frame_layout.addWidget(name) + frame_layout.addWidget(line_edit) selection_layout.addWidget(selection_name) selection_layout.addWidget(has_selection) button_layout.addWidget(button) button_layout.addWidget(cancel) - layout.addLayout(line_layout) + layout.addLayout(frame_layout) layout.addLayout(selection_layout) layout.addLayout(button_layout) # Default size @@ -73,7 +95,6 @@ class CustomScriptDialog(QtWidgets.QDialog): "button": button, "cancel": cancel } - # Signals has_selection.toggled.connect(self.emit_click_with_state) line_edit.textChanged.connect(self.on_line_edit_changed) @@ -115,18 +136,34 @@ class CustomScriptDialog(QtWidgets.QDialog): Raises the parent (if any) """ + frame_range = self.widgets['line_edit'].text() + selected = self.widgets["selection"].isChecked() + pattern = r"^(?P-?[0-9]+)(?:(?:-+)(?P-?[0-9]+))?$" + match = re.match(pattern, frame_range) + frame_start = int(match.group("start")) + frame_end = int(match.group("end")) + if not nuke.allNodes("Read"): + return + for read_node in nuke.allNodes("Read"): + if selected: + if not nuke.selectedNodes(): + return + if read_node in nuke.selectedNodes(): + read_node["frame_mode"].setValue("start_at") + read_node["frame"].setValue(frame_range) + read_node["first"].setValue(frame_start) + read_node["last"].setValue(frame_end) + else: + read_node["frame_mode"].setValue("start_at") + read_node["frame"].setValue(frame_range) + read_node["first"].setValue(frame_start) + read_node["last"].setValue(frame_end) - parent = self.parent() self.close() - # Trigger the signal - self.on_clicked.emit() - - if parent: - parent.raise_() + return False def showEvent(self, event): - # Position popup based on contents on show event return super(CustomScriptDialog, self).showEvent(event) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index f01bdf7d50..287d13e5c9 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -222,6 +222,13 @@ "title": "OpenPype Docs", "command": "import webbrowser;webbrowser.open(url='https://openpype.io/docs/artist_hosts_nuke_tut')", "tooltip": "Open the OpenPype Nuke user doc page" + }, + { + "type": "action", + "sourcetype": "python", + "title": "Set Frame Range(Read Node)", + "command": "from openpype.hosts.nuke.startup import custom_popup;from openpype.hosts.nuke.startup.custom_popup import get_main_window;custom_popup.CustomScriptDialog(parent=get_main_window()).show();", + "tooltip": "Set Frame Range for Read Node(s)" } ] }, From 3556b58fdc593eef0c22eb3d6b357e0c0ac0be7c Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 30 May 2023 21:46:25 +0800 Subject: [PATCH 118/147] hound fix --- openpype/hosts/nuke/api/lib.py | 1 - openpype/hosts/nuke/startup/custom_popup.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 59a63d1373..a439142051 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2358,7 +2358,6 @@ class WorkfileSettings(object): # add colorspace menu item self.set_colorspace() - def set_favorites(self): from .utils import set_context_favorites diff --git a/openpype/hosts/nuke/startup/custom_popup.py b/openpype/hosts/nuke/startup/custom_popup.py index c85577133c..dfbd590e03 100644 --- a/openpype/hosts/nuke/startup/custom_popup.py +++ b/openpype/hosts/nuke/startup/custom_popup.py @@ -21,6 +21,7 @@ def get_main_window(): break return main_window + class CustomScriptDialog(QtWidgets.QDialog): """A Popup that moves itself to bottom right of screen on show event. @@ -34,7 +35,6 @@ class CustomScriptDialog(QtWidgets.QDialog): context = None - def __init__(self, parent=None, *args, **kwargs): super(CustomScriptDialog, self).__init__(parent=parent, *args, From dddfeecceb4c711e1d8b848c8454d529992e6182 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 30 May 2023 21:47:03 +0800 Subject: [PATCH 119/147] hound fix --- openpype/hosts/nuke/startup/custom_popup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/nuke/startup/custom_popup.py b/openpype/hosts/nuke/startup/custom_popup.py index dfbd590e03..d400ed913c 100644 --- a/openpype/hosts/nuke/startup/custom_popup.py +++ b/openpype/hosts/nuke/startup/custom_popup.py @@ -34,7 +34,6 @@ class CustomScriptDialog(QtWidgets.QDialog): on_line_changed = QtCore.Signal(str) context = None - def __init__(self, parent=None, *args, **kwargs): super(CustomScriptDialog, self).__init__(parent=parent, *args, From 8f0821ab327db90a6fba5d95594bdcc461fb33e3 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 30 May 2023 22:12:47 +0800 Subject: [PATCH 120/147] the dialog closes as usual by clicking execute button when there is no nuke nodes or no nuke nodes by selection --- openpype/hosts/nuke/startup/custom_popup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/nuke/startup/custom_popup.py b/openpype/hosts/nuke/startup/custom_popup.py index d400ed913c..57d79f99ff 100644 --- a/openpype/hosts/nuke/startup/custom_popup.py +++ b/openpype/hosts/nuke/startup/custom_popup.py @@ -142,10 +142,12 @@ class CustomScriptDialog(QtWidgets.QDialog): frame_start = int(match.group("start")) frame_end = int(match.group("end")) if not nuke.allNodes("Read"): + self.close() return for read_node in nuke.allNodes("Read"): if selected: if not nuke.selectedNodes(): + self.close() return if read_node in nuke.selectedNodes(): read_node["frame_mode"].setValue("start_at") From de72f26f9451f48608229cc85868c1545a201afd Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 30 May 2023 17:09:54 +0200 Subject: [PATCH 121/147] adding check also against class attribute --- openpype/plugins/publish/collect_frames_fix.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/collect_frames_fix.py b/openpype/plugins/publish/collect_frames_fix.py index 837738eb06..ca1ccc19fd 100644 --- a/openpype/plugins/publish/collect_frames_fix.py +++ b/openpype/plugins/publish/collect_frames_fix.py @@ -66,7 +66,7 @@ class CollectFramesFixDef( self.log.debug("last_version_published_files::{}".format( instance.data["last_version_published_files"])) - if rewrite_version: + if self.rewrite_version_enable and rewrite_version: instance.data["version"] = version["name"] # limits triggering version validator instance.data.pop("latestVersion") From 04e6f5f4bb844b11dbb93475f5d023c2125651ff Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 30 May 2023 17:16:18 +0200 Subject: [PATCH 122/147] :bug: fix support for separate AOVs and some style issues --- openpype/hosts/max/api/lib_renderproducts.py | 76 +++++++++---------- .../max/plugins/publish/collect_render.py | 10 ++- 2 files changed, 46 insertions(+), 40 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index a6427bf7c5..81057db733 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -3,22 +3,20 @@ # arnold # https://help.autodesk.com/view/ARNOL/ENU/?guid=arnold_for_3ds_max_ax_maxscript_commands_ax_renderview_commands_html import os + from pymxs import runtime as rt -from openpype.hosts.max.api.lib import ( - get_current_renderer -) -from openpype.settings import get_project_settings + +from openpype.hosts.max.api.lib import get_current_renderer from openpype.pipeline import legacy_io +from openpype.settings import get_project_settings class RenderProducts(object): def __init__(self, project_settings=None): - self._project_settings = project_settings - if not self._project_settings: - self._project_settings = get_project_settings( - legacy_io.Session["AVALON_PROJECT"] - ) + self._project_settings = project_settings or get_project_settings( + legacy_io.Session["AVALON_PROJECT"] + ) def get_beauty(self, container): render_dir = os.path.dirname(rt.rendOutputFilename) @@ -29,14 +27,14 @@ class RenderProducts(object): setting = self._project_settings img_fmt = setting["max"]["RenderSettings"]["image_format"] # noqa - startFrame = int(rt.rendStart) - endFrame = int(rt.rendEnd) + 1 + start_frame = int(rt.rendStart) + end_frame = int(rt.rendEnd) + 1 - render_dict = { + return { "beauty": self.get_expected_beauty( - output_file, startFrame, endFrame, img_fmt) + output_file, start_frame, end_frame, img_fmt + ) } - return render_dict def get_aovs(self, container): render_dir = os.path.dirname(rt.rendOutputFilename) @@ -47,8 +45,8 @@ class RenderProducts(object): setting = self._project_settings img_fmt = setting["max"]["RenderSettings"]["image_format"] # noqa - startFrame = int(rt.rendStart) - endFrame = int(rt.rendEnd) + 1 + start_frame = int(rt.rendStart) + end_frame = int(rt.rendEnd) + 1 renderer_class = get_current_renderer() renderer = str(renderer_class).split(":")[0] render_dict = {} @@ -65,38 +63,40 @@ class RenderProducts(object): for name in render_name: render_dict.update({ name: self.get_expected_render_elements( - output_file, name, startFrame, - endFrame, img_fmt) + output_file, name, start_frame, + end_frame, img_fmt) }) - if renderer == "Redshift_Renderer": + elif renderer == "Redshift_Renderer": render_name = self.get_render_elements_name() if render_name: - rs_AovFiles = rt.RedShift_Renderer().separateAovFiles - if img_fmt == "exr" and not rs_AovFiles: + rs_aov_files = rt.Execute("renderers.current.separateAovFiles") + # this doesn't work, always returns False + # rs_AovFiles = rt.RedShift_Renderer().separateAovFiles + if img_fmt == "exr" and not rs_aov_files: for name in render_name: if name == "RsCryptomatte": render_dict.update({ name: self.get_expected_render_elements( - output_file, name, startFrame, - endFrame, img_fmt) + output_file, name, start_frame, + end_frame, img_fmt) }) else: for name in render_name: render_dict.update({ name: self.get_expected_render_elements( - output_file, name, startFrame, - endFrame, img_fmt) + output_file, name, start_frame, + end_frame, img_fmt) }) - if renderer == "Arnold": + elif renderer == "Arnold": render_name = self.get_arnold_product_name() if render_name: for name in render_name: render_dict.update({ name: self.get_expected_arnold_product( - output_file, name, startFrame, endFrame, img_fmt) + output_file, name, start_frame, end_frame, img_fmt) }) - if renderer in [ + elif renderer in [ "V_Ray_6_Hotfix_3", "V_Ray_GPU_6_Hotfix_3" ]: @@ -106,15 +106,15 @@ class RenderProducts(object): for name in render_name: render_dict.update({ name: self.get_expected_render_elements( - output_file, name, startFrame, - endFrame, img_fmt) # noqa + output_file, name, start_frame, + end_frame, img_fmt) # noqa }) return render_dict - def get_expected_beauty(self, folder, startFrame, endFrame, fmt): + def get_expected_beauty(self, folder, start_frame, end_frame, fmt): beauty_frame_range = [] - for f in range(startFrame, endFrame): + for f in range(start_frame, end_frame): frame = "%04d" % f beauty_output = f"{folder}.{frame}.{fmt}" beauty_output = beauty_output.replace("\\", "/") @@ -134,19 +134,17 @@ class RenderProducts(object): return for i in range(aov_group_num): # get the specific AOV group - for aov in aov_mgr.drivers[i].aov_list: - aov_name.append(aov.name) - + aov_name.extend(aov.name for aov in aov_mgr.drivers[i].aov_list) # close the AOVs manager window amw.close() return aov_name def get_expected_arnold_product(self, folder, name, - startFrame, endFrame, fmt): + start_frame, end_frame, fmt): """Get all the expected Arnold AOVs""" aov_list = [] - for f in range(startFrame, endFrame): + for f in range(start_frame, end_frame): frame = "%04d" % f render_element = f"{folder}_{name}.{frame}.{fmt}" render_element = render_element.replace("\\", "/") @@ -171,10 +169,10 @@ class RenderProducts(object): return render_name def get_expected_render_elements(self, folder, name, - startFrame, endFrame, fmt): + start_frame, end_frame, fmt): """Get all the expected render element output files. """ render_elements = [] - for f in range(startFrame, endFrame): + for f in range(start_frame, end_frame): frame = "%04d" % f render_element = f"{folder}_{name}.{frame}.{fmt}" render_element = render_element.replace("\\", "/") diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 77ab5f654d..db5c84fad9 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -66,7 +66,7 @@ class CollectRender(pyblish.api.InstancePlugin): instance.data["attachTo"] = [] renderer_class = get_current_renderer() renderer = str(renderer_class).split(":")[0] - # also need to get the render dir for coversion + # also need to get the render dir for conversion data = { "asset": asset, "subset": str(instance.name), @@ -84,4 +84,12 @@ class CollectRender(pyblish.api.InstancePlugin): "farm": True } instance.data.update(data) + + # TODO: this should be unified with maya and its "multipart" flag + # on instance. + if renderer == "Redshift_Renderer": + instance.data.update( + {"separateAovFiles": rt.Execute( + "renderers.current.separateAovFiles")}) + self.log.info("data: {0}".format(data)) From 4d76c2520f254991da405dc463418d9bb6e2cfc4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 30 May 2023 17:19:00 +0200 Subject: [PATCH 123/147] cleanup --- .../plugins/publish/collect_frames_fix.py | 62 ++++++++++--------- 1 file changed, 34 insertions(+), 28 deletions(-) diff --git a/openpype/plugins/publish/collect_frames_fix.py b/openpype/plugins/publish/collect_frames_fix.py index ca1ccc19fd..86e727b053 100644 --- a/openpype/plugins/publish/collect_frames_fix.py +++ b/openpype/plugins/publish/collect_frames_fix.py @@ -35,41 +35,47 @@ class CollectFramesFixDef( rewrite_version = attribute_values.get("rewrite_version") - if frames_to_fix: - instance.data["frames_to_fix"] = frames_to_fix + if not frames_to_fix: + return - subset_name = instance.data["subset"] - asset_name = instance.data["asset"] + instance.data["frames_to_fix"] = frames_to_fix - project_entity = instance.data["projectEntity"] - project_name = project_entity["name"] + subset_name = instance.data["subset"] + asset_name = instance.data["asset"] - version = get_last_version_by_subset_name(project_name, - subset_name, - asset_name=asset_name) - if not version: - self.log.warning("No last version found, " - "re-render not possible") - return + project_entity = instance.data["projectEntity"] + project_name = project_entity["name"] - representations = get_representations(project_name, - version_ids=[version["_id"]]) - published_files = [] - for repre in representations: - if repre["context"]["family"] not in self.families: - continue + version = get_last_version_by_subset_name( + project_name, + subset_name, + asset_name=asset_name + ) + if not version: + self.log.warning( + "No last version found, re-render not possible" + ) + return - for file_info in repre.get("files"): - published_files.append(file_info["path"]) + representations = get_representations( + project_name, version_ids=[version["_id"]] + ) + published_files = [] + for repre in representations: + if repre["context"]["family"] not in self.families: + continue - instance.data["last_version_published_files"] = published_files - self.log.debug("last_version_published_files::{}".format( - instance.data["last_version_published_files"])) + for file_info in repre.get("files"): + published_files.append(file_info["path"]) - if self.rewrite_version_enable and rewrite_version: - instance.data["version"] = version["name"] - # limits triggering version validator - instance.data.pop("latestVersion") + instance.data["last_version_published_files"] = published_files + self.log.debug("last_version_published_files::{}".format( + instance.data["last_version_published_files"])) + + if self.rewrite_version_enable and rewrite_version: + instance.data["version"] = version["name"] + # limits triggering version validator + instance.data.pop("latestVersion") @classmethod def get_attribute_defs(cls): From be523bc5ec967efce3181668a932eb644a5e8f59 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 30 May 2023 16:49:49 +0100 Subject: [PATCH 124/147] Use temp folder to copy commandlet project --- openpype/hosts/unreal/ue_workers.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/ue_workers.py b/openpype/hosts/unreal/ue_workers.py index e7a690ac9c..2b7e1375e6 100644 --- a/openpype/hosts/unreal/ue_workers.py +++ b/openpype/hosts/unreal/ue_workers.py @@ -6,6 +6,8 @@ import subprocess from distutils import dir_util from pathlib import Path from typing import List, Union +import tempfile +from distutils.dir_util import copy_tree import openpype.hosts.unreal.lib as ue_lib @@ -90,9 +92,20 @@ class UEProjectGenerationWorker(QtCore.QObject): ("Generating a new UE project ... 1 out of " f"{stage_count}")) + # Need to copy the commandlet project to a temporary folder where + # users don't need admin rights to write to. + cmdlet_tmp = tempfile.TemporaryDirectory() + cmdlet_filename = cmdlet_project.name + cmdlet_dir = cmdlet_project.parent.as_posix() + cmdlet_tmp_name = Path(cmdlet_tmp.name) + cmdlet_tmp_file = cmdlet_tmp_name.joinpath(cmdlet_filename) + copy_tree( + cmdlet_dir, + cmdlet_tmp_name.as_posix()) + commandlet_cmd = [ f"{ue_editor_exe.as_posix()}", - f"{cmdlet_project.as_posix()}", + f"{cmdlet_tmp_file.as_posix()}", "-run=AyonGenerateProject", f"{project_file.resolve().as_posix()}", ] @@ -111,6 +124,8 @@ class UEProjectGenerationWorker(QtCore.QObject): gen_process.stdout.close() return_code = gen_process.wait() + cmdlet_tmp.cleanup() + if return_code and return_code != 0: msg = ( f"Failed to generate {self.project_name} " From a215e4f00665f779f1056f5a128b3e2674e3c68f Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 31 May 2023 03:26:47 +0000 Subject: [PATCH 125/147] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index c24388b2ff..5c7105e7e0 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.9-nightly.1" +__version__ = "3.15.9-nightly.2" From e8b47be8d562e8bb1edd77dd4aebf6e75f6fc720 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 31 May 2023 03:27:31 +0000 Subject: [PATCH 126/147] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 54a4ee6ac0..0036e121b7 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.15.9-nightly.2 - 3.15.9-nightly.1 - 3.15.8 - 3.15.8-nightly.3 @@ -134,7 +135,6 @@ body: - 3.14.2 - 3.14.2-nightly.5 - 3.14.2-nightly.4 - - 3.14.2-nightly.3 validations: required: true - type: dropdown From e6d10fa3358b61eae7c9461da2119af72adf9be6 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Wed, 31 May 2023 10:37:21 +0100 Subject: [PATCH 127/147] Update settings_project_global.md (#5045) --- website/docs/project_settings/settings_project_global.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index c17f707830..7bd24a5773 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -63,7 +63,7 @@ Example here describes use case for creation of new color coded review of png im ![global_oiio_transcode](assets/global_oiio_transcode.png) Another use case is to transcode in Maya only `beauty` render layers and use collected `Display` and `View` colorspaces from DCC. -![global_oiio_transcode_in_Maya](assets/global_oiio_transcode.png)n +![global_oiio_transcode_in_Maya](assets/global_oiio_transcode2.png) ## Profile filters From e22d1bf78b4e9d4aeeb7c0295c8bb92b1c5595d9 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 31 May 2023 10:45:47 +0100 Subject: [PATCH 128/147] Set dev mode off by default --- openpype/settings/defaults/project_settings/unreal.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/unreal.json b/openpype/settings/defaults/project_settings/unreal.json index 737a17d289..92bdb468ba 100644 --- a/openpype/settings/defaults/project_settings/unreal.json +++ b/openpype/settings/defaults/project_settings/unreal.json @@ -15,6 +15,6 @@ "preroll_frames": 0, "render_format": "png", "project_setup": { - "dev_mode": true + "dev_mode": false } } From c2b753326fb6cb6460a43e401fb5731cdee1fabb Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 31 May 2023 11:59:21 +0100 Subject: [PATCH 129/147] Check if the Unreal app name follows the right format --- openpype/hosts/unreal/addon.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/openpype/hosts/unreal/addon.py b/openpype/hosts/unreal/addon.py index 1119b5c16c..fddceb00a8 100644 --- a/openpype/hosts/unreal/addon.py +++ b/openpype/hosts/unreal/addon.py @@ -1,5 +1,7 @@ import os +import re from openpype.modules import IHostAddon, OpenPypeModule +from openpype.widgets.message_window import Window UNREAL_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -19,6 +21,18 @@ class UnrealAddon(OpenPypeModule, IHostAddon): from .lib import get_compatible_integration + pattern = re.compile(r'^\d+-\d+$') + + if not pattern.match(app.name): + Window( + parent=None, + title="Unreal application name format", + message="Unreal application name must be in format '5-0' or '5-1'", + level="critical") + raise ValueError( + "Unreal application name must be in format '5-0' or '5-1'" + ) + ue_version = app.name.replace("-", ".") unreal_plugin_path = os.path.join( UNREAL_ROOT_DIR, "integration", "UE_{}".format(ue_version), "Ayon" From 142ae35d9b81b5325c462b67cc870c959a801524 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 31 May 2023 12:13:00 +0100 Subject: [PATCH 130/147] Hound fixes --- openpype/hosts/unreal/addon.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/unreal/addon.py b/openpype/hosts/unreal/addon.py index fddceb00a8..16f6fcf27c 100644 --- a/openpype/hosts/unreal/addon.py +++ b/openpype/hosts/unreal/addon.py @@ -24,14 +24,13 @@ class UnrealAddon(OpenPypeModule, IHostAddon): pattern = re.compile(r'^\d+-\d+$') if not pattern.match(app.name): + msg = "Unreal application name must be in format '5-0' or '5-1'" Window( parent=None, title="Unreal application name format", - message="Unreal application name must be in format '5-0' or '5-1'", + message=msg, level="critical") - raise ValueError( - "Unreal application name must be in format '5-0' or '5-1'" - ) + raise ValueError(msg) ue_version = app.name.replace("-", ".") unreal_plugin_path = os.path.join( From 11728bae0e4f1a539d5959ec3cfe9fe523fe356a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 31 May 2023 20:02:05 +0800 Subject: [PATCH 131/147] roy's comment and uses import instead of from..import --- openpype/hosts/nuke/startup/custom_popup.py | 17 ++++------------- .../defaults/project_settings/nuke.json | 2 +- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/nuke/startup/custom_popup.py b/openpype/hosts/nuke/startup/custom_popup.py index 57d79f99ff..7f60fdc70b 100644 --- a/openpype/hosts/nuke/startup/custom_popup.py +++ b/openpype/hosts/nuke/startup/custom_popup.py @@ -23,10 +23,7 @@ def get_main_window(): class CustomScriptDialog(QtWidgets.QDialog): - """A Popup that moves itself to bottom right of screen on show event. - - The UI contains a message label and a red highlighted button to "show" - or perform another custom action from this pop-up. + """A Custom Popup For Nuke Read Node """ @@ -95,7 +92,7 @@ class CustomScriptDialog(QtWidgets.QDialog): "cancel": cancel } # Signals - has_selection.toggled.connect(self.emit_click_with_state) + has_selection.toggled.connect(self.on_checked_changed) line_edit.textChanged.connect(self.on_line_edit_changed) button.clicked.connect(self._on_clicked) cancel.clicked.connect(self.close) @@ -104,10 +101,9 @@ class CustomScriptDialog(QtWidgets.QDialog): self.setWindowTitle("Custom Popup") def update_values(self): - self.widgets["selection"].isChecked() + return self.widgets["selection"].isChecked() - def emit_click_with_state(self): - """Emit the on_clicked signal with the toggled state""" + def on_checked_changed(self): checked = self.widgets["selection"].isChecked() return checked @@ -164,11 +160,6 @@ class CustomScriptDialog(QtWidgets.QDialog): return False - def showEvent(self, event): - # Position popup based on contents on show event - return super(CustomScriptDialog, self).showEvent(event) - - @contextlib.contextmanager def application(): app = QtWidgets.QApplication(sys.argv) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 287d13e5c9..c116540a99 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -227,7 +227,7 @@ "type": "action", "sourcetype": "python", "title": "Set Frame Range(Read Node)", - "command": "from openpype.hosts.nuke.startup import custom_popup;from openpype.hosts.nuke.startup.custom_popup import get_main_window;custom_popup.CustomScriptDialog(parent=get_main_window()).show();", + "command": "import openpype.hosts.nuke.startup.custom_popup as popup;popup.CustomScriptDialog(parent=popup.get_main_window()).show();", "tooltip": "Set Frame Range for Read Node(s)" } ] From 803dd565751bb65a6ef5b97e7c6f5447f4534717 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 31 May 2023 20:03:18 +0800 Subject: [PATCH 132/147] hound fix --- openpype/hosts/nuke/startup/custom_popup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/nuke/startup/custom_popup.py b/openpype/hosts/nuke/startup/custom_popup.py index 7f60fdc70b..76d5a25596 100644 --- a/openpype/hosts/nuke/startup/custom_popup.py +++ b/openpype/hosts/nuke/startup/custom_popup.py @@ -160,6 +160,7 @@ class CustomScriptDialog(QtWidgets.QDialog): return False + @contextlib.contextmanager def application(): app = QtWidgets.QApplication(sys.argv) From 183b2866d9e1f1f7b611fa6860c1edc315798b66 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 31 May 2023 20:14:57 +0800 Subject: [PATCH 133/147] style fix --- openpype/settings/defaults/project_settings/nuke.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index c116540a99..35e5b1975c 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -226,7 +226,7 @@ { "type": "action", "sourcetype": "python", - "title": "Set Frame Range(Read Node)", + "title": "Set Frame Range (Read Node)", "command": "import openpype.hosts.nuke.startup.custom_popup as popup;popup.CustomScriptDialog(parent=popup.get_main_window()).show();", "tooltip": "Set Frame Range for Read Node(s)" } From 0635c39a37094bf4f78898a9a179fbf657baf021 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 31 May 2023 14:32:24 +0100 Subject: [PATCH 134/147] Improved error message --- openpype/hosts/unreal/addon.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/addon.py b/openpype/hosts/unreal/addon.py index 16f6fcf27c..ed23950b35 100644 --- a/openpype/hosts/unreal/addon.py +++ b/openpype/hosts/unreal/addon.py @@ -24,7 +24,10 @@ class UnrealAddon(OpenPypeModule, IHostAddon): pattern = re.compile(r'^\d+-\d+$') if not pattern.match(app.name): - msg = "Unreal application name must be in format '5-0' or '5-1'" + msg = ( + "Unreal application key in the settings must be in format" + "'5-0' or '5-1'" + ) Window( parent=None, title="Unreal application name format", From 396486067bd4c33f0ac6ef75f0676e05b9b6c985 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 31 May 2023 13:43:50 +0000 Subject: [PATCH 135/147] [Automated] Release --- CHANGELOG.md | 335 ++++++++++++++++++++++++++++++++++++++++++++ openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 337 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a33904735b..ec6544e659 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,341 @@ # Changelog +## [3.15.9](https://github.com/ynput/OpenPype/tree/3.15.9) + + +[Full Changelog](https://github.com/ynput/OpenPype/compare/3.15.8...3.15.9) + +### **πŸ†• New features** + + +
+Blender: Implemented Loading of Alembic Camera #4990 + +Implemented loading of Alembic cameras in Blender. + + +___ + +
+ + +
+Unreal: Implemented Creator, Loader and Extractor for Levels #5008 + +Creator, Loader and Extractor for Unreal Levels have been implemented. + + +___ + +
+ +### **πŸš€ Enhancements** + + +
+Blender: Added setting for base unit scale #4987 + +A setting for the base unit scale has been added for Blender.The unit scale is automatically applied when opening a file or creating a new one. + + +___ + +
+ + +
+Unreal: Changed naming and path of Camera Levels #5010 + +The levels created for the camera in Unreal now include `_camera` in the name, to be better identifiable, and are placed in the camera folder. + + +___ + +
+ + +
+Settings: Added option to nest settings templates #5022 + +It is possible to nest settings templates in another templates. + + +___ + +
+ + +
+Enhancement/publisher: Remove "hit play to continue" label on continue #5029 + +Remove "hit play to continue" message on continue so that it doesn't show anymore when play was clicked. + + +___ + +
+ + +
+Ftrack: Limit number of ftrack events to query at once #5033 + +Limit the amount of ftrack events received from mongo at once to 100. + + +___ + +
+ + +
+General: Small code cleanups #5034 + +Small code cleanup and updates. + + +___ + +
+ + +
+Global: collect frames to fix with settings #5036 + +Settings for `Collect Frames to Fix` will allow disable per project the plugin. Also `Rewriting latest version` attribute is hiddable from settings. + + +___ + +
+ + +
+General: Publish plugin apply settings can expect only project settings #5037 + +Only project settings are passed to optional `apply_settings` method, if the method expects only one argument. + + +___ + +
+ +### **πŸ› Bug fixes** + + +
+Maya: Load Assembly fix invalid imports #4859 + +Refactors imports so they are now correct. + + +___ + +
+ + +
+Maya: Skipping rendersetup for members. #4973 + +When publishing a `rendersetup`, the objectset is and should be empty. + + +___ + +
+ + +
+Maya: Validate Rig Output IDs #5016 + +Absolute names of node were not used, so plugin did not fetch the nodes properly.Also missed pymel command. + + +___ + +
+ + +
+Deadline: escape rootless path in publish job #4910 + +If the publish path on Deadline job contains spaces or other characters, command was failing because the path wasn't properly escaped. This is fixing it. + + +___ + +
+ + +
+General: Company name and URL changed #4974 + +The current records were obsolete in inno_setup, changed to the up-to-date. +___ + +
+ + +
+Unreal: Fix usage of 'get_full_path' function #5014 + +This PR changes all the occurrences of `get_full_path` functions to alternatives to get the path of the objects. + + +___ + +
+ + +
+Unreal: Fix sequence frames validator to use correct data #5021 + +Fix sequence frames validator to use clipIn and clipOut data instead of frameStart and frameEnd. + + +___ + +
+ + +
+Unreal: Fix render instances collection to use correct data #5023 + +Fix render instances collection to use `frameStart` and `frameEnd` from the Project Manager, instead of the sequence's ones. + + +___ + +
+ + +
+Resolve: loader is opening even if no timeline in project #5025 + +Loader is opening now even no timeline is available in a project. + + +___ + +
+ + +
+nuke: callback for dirmapping is on demand #5030 + +Nuke was slowed down on processing due this callback. Since it is disabled by default it made sense to add it only on demand. + + +___ + +
+ + +
+Publisher: UI works with instances without label #5032 + +Publisher UI does not crash if instance don't have filled 'label' key in instance data. + + +___ + +
+ + +
+Publisher: Call explicitly prepared tab methods #5044 + +It is not possible to go to Create tab during publishing from OpenPype menu. + + +___ + +
+ + +
+Ftrack: Role names are not case sensitive in ftrack event server status action #5058 + +Event server status action is not case sensitive for role names of user. + + +___ + +
+ + +
+Publisher: Fix border widget #5063 + +Fixed border lines in Publisher UI to be painted correctly with correct indentation and size. + + +___ + +
+ + +
+Unreal: Fix Commandlet Project and Permissions #5066 + +Fix problem when creating an Unreal Project when Commandlet Project is in a protected location. + + +___ + +
+ + +
+Unreal: Added verification for Unreal app name format #5070 + +The Unreal app name is used to determine the Unreal version folder, so it is necessary that if follows the format `x-x`, where `x` is any integer. This PR adds a verification that the app name follows that format. + + +___ + +
+ +### **πŸ“ƒ Documentation** + + +
+Docs: Display wrong image in ExtractOIIOTranscode #5045 + +Wrong image display in `https://openpype.io/docs/project_settings/settings_project_global#extract-oiio-transcode`. + + +___ + +
+ +### **Merged pull requests** + + +
+Drop-down menu to list all families in create placeholder #4928 + +Currently in the create placeholder window, we need to write the family manually. This replace the text field by an enum field with all families for the current software. + + +___ + +
+ + +
+add sync to specific projects or listen only #4919 + +Extend kitsu sync service with additional arguments to sync specific projects. + + +___ + +
+ + + + ## [3.15.8](https://github.com/ynput/OpenPype/tree/3.15.8) diff --git a/openpype/version.py b/openpype/version.py index 5c7105e7e0..dd23138dee 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.9-nightly.2" +__version__ = "3.15.9" diff --git a/pyproject.toml b/pyproject.toml index a72a3d66d7..633899d3a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.15.8" # OpenPype +version = "3.15.9" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 776dcb5a6fcf65eec08f95655c870d7e43ec4ce9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 31 May 2023 13:44:50 +0000 Subject: [PATCH 136/147] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 0036e121b7..aa5b8decdc 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.15.9 - 3.15.9-nightly.2 - 3.15.9-nightly.1 - 3.15.8 @@ -134,7 +135,6 @@ body: - 3.14.3-nightly.1 - 3.14.2 - 3.14.2-nightly.5 - - 3.14.2-nightly.4 validations: required: true - type: dropdown From 3955c466c3a0f01e20bf95bfa594b561884517e5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 31 May 2023 17:17:35 +0200 Subject: [PATCH 137/147] fix apply settings on hiero loader (#5073) --- openpype/hosts/hiero/plugins/load/load_clip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/hiero/plugins/load/load_clip.py b/openpype/hosts/hiero/plugins/load/load_clip.py index 77844d2448..c9bebfa8b2 100644 --- a/openpype/hosts/hiero/plugins/load/load_clip.py +++ b/openpype/hosts/hiero/plugins/load/load_clip.py @@ -41,8 +41,8 @@ class LoadClip(phiero.SequenceLoader): clip_name_template = "{asset}_{subset}_{representation}" + @classmethod def apply_settings(cls, project_settings, system_settings): - plugin_type_settings = ( project_settings .get("hiero", {}) From a30ba51508f03c1adbac8c434432a80184d0665b Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 1 Jun 2023 00:11:30 +0800 Subject: [PATCH 138/147] use nukescript's panel --- openpype/hosts/nuke/startup/custom_popup.py | 174 ------------------ .../startup/ops_frame_setting_for_read.py | 46 +++++ .../defaults/project_settings/nuke.json | 2 +- 3 files changed, 47 insertions(+), 175 deletions(-) delete mode 100644 openpype/hosts/nuke/startup/custom_popup.py create mode 100644 openpype/hosts/nuke/startup/ops_frame_setting_for_read.py diff --git a/openpype/hosts/nuke/startup/custom_popup.py b/openpype/hosts/nuke/startup/custom_popup.py deleted file mode 100644 index 76d5a25596..0000000000 --- a/openpype/hosts/nuke/startup/custom_popup.py +++ /dev/null @@ -1,174 +0,0 @@ -import sys -import contextlib -import re -import nuke -from PySide2 import QtCore, QtWidgets - - -def get_main_window(): - """Acquire Nuke's main window""" - main_window = None - if main_window is None: - - top_widgets = QtWidgets.QApplication.topLevelWidgets() - name = "Foundry::UI::DockMainWindow" - for widget in top_widgets: - if ( - widget.inherits("QMainWindow") - and widget.metaObject().className() == name - ): - main_window = widget - break - return main_window - - -class CustomScriptDialog(QtWidgets.QDialog): - """A Custom Popup For Nuke Read Node - - """ - - on_clicked = QtCore.Signal() - on_line_changed = QtCore.Signal(str) - context = None - - def __init__(self, parent=None, *args, **kwargs): - super(CustomScriptDialog, self).__init__(parent=parent, - *args, - **kwargs) - self.setContentsMargins(0, 0, 0, 0) - - # Layout - layout = QtWidgets.QVBoxLayout(self) - frame_layout = QtWidgets.QHBoxLayout() - frame_layout.setContentsMargins(10, 5, 10, 10) - selection_layout = QtWidgets.QHBoxLayout() - selection_layout.setContentsMargins(10, 5, 10, 10) - button_layout = QtWidgets.QHBoxLayout() - button_layout.setContentsMargins(10, 5, 10, 10) - - # Increase spacing slightly for readability - frame_layout.setSpacing(10) - button_layout.setSpacing(10) - name = QtWidgets.QLabel("Frame Range: ") - name.setStyleSheet(""" - QLabel { - font-size: 12px; - } - """) - line_edit = QtWidgets.QLineEdit( - "%s-%s" % (nuke.root().firstFrame(), - nuke.root().lastFrame())) - selection_name = QtWidgets.QLabel("Use Selection") - selection_name.setStyleSheet(""" - QLabel { - font-size: 12px; - } - """) - has_selection = QtWidgets.QCheckBox() - button = QtWidgets.QPushButton("Execute") - button.setSizePolicy(QtWidgets.QSizePolicy.Maximum, - QtWidgets.QSizePolicy.Maximum) - cancel = QtWidgets.QPushButton("Cancel") - cancel.setSizePolicy(QtWidgets.QSizePolicy.Maximum, - QtWidgets.QSizePolicy.Maximum) - - frame_layout.addWidget(name) - frame_layout.addWidget(line_edit) - selection_layout.addWidget(selection_name) - selection_layout.addWidget(has_selection) - button_layout.addWidget(button) - button_layout.addWidget(cancel) - layout.addLayout(frame_layout) - layout.addLayout(selection_layout) - layout.addLayout(button_layout) - # Default size - self.resize(100, 40) - - self.widgets = { - "name": name, - "line_edit": line_edit, - "selection": has_selection, - "button": button, - "cancel": cancel - } - # Signals - has_selection.toggled.connect(self.on_checked_changed) - line_edit.textChanged.connect(self.on_line_edit_changed) - button.clicked.connect(self._on_clicked) - cancel.clicked.connect(self.close) - self.update_values() - # Set default title - self.setWindowTitle("Custom Popup") - - def update_values(self): - return self.widgets["selection"].isChecked() - - def on_checked_changed(self): - checked = self.widgets["selection"].isChecked() - return checked - - def set_name(self, name): - self.widgets['name'].setText(name) - - def set_line_edit(self, line_edit): - self.widgets['line_edit'].setText(line_edit) - print(line_edit) - - def setButtonText(self, text): - self.widgets["button"].setText(text) - - def setCancelText(self, text): - self.widgets["cancel"].setText(text) - - def on_line_edit_changed(self): - line_edit = self.widgets['line_edit'].text() - self.on_line_changed.emit(line_edit) - return self.set_line_edit(line_edit) - - def _on_clicked(self): - """Callback for when the 'show' button is clicked. - - Raises the parent (if any) - - """ - frame_range = self.widgets['line_edit'].text() - selected = self.widgets["selection"].isChecked() - pattern = r"^(?P-?[0-9]+)(?:(?:-+)(?P-?[0-9]+))?$" - match = re.match(pattern, frame_range) - frame_start = int(match.group("start")) - frame_end = int(match.group("end")) - if not nuke.allNodes("Read"): - self.close() - return - for read_node in nuke.allNodes("Read"): - if selected: - if not nuke.selectedNodes(): - self.close() - return - if read_node in nuke.selectedNodes(): - read_node["frame_mode"].setValue("start_at") - read_node["frame"].setValue(frame_range) - read_node["first"].setValue(frame_start) - read_node["last"].setValue(frame_end) - else: - read_node["frame_mode"].setValue("start_at") - read_node["frame"].setValue(frame_range) - read_node["first"].setValue(frame_start) - read_node["last"].setValue(frame_end) - - self.close() - - return False - - -@contextlib.contextmanager -def application(): - app = QtWidgets.QApplication(sys.argv) - yield - app.exec_() - - -if __name__ == "__main__": - with application(): - dialog = CustomScriptDialog() - dialog.show() diff --git a/openpype/hosts/nuke/startup/ops_frame_setting_for_read.py b/openpype/hosts/nuke/startup/ops_frame_setting_for_read.py new file mode 100644 index 0000000000..153effc7ad --- /dev/null +++ b/openpype/hosts/nuke/startup/ops_frame_setting_for_read.py @@ -0,0 +1,46 @@ +import nuke +import nukescripts +import re + + +class FrameSettingsPanel(nukescripts.PythonPanel): + def __init__(self, node): + nukescripts.PythonPanel.__init__(self, 'Frame Range') + self.read_node = node + # CREATE KNOBS + self.range = nuke.String_Knob('fRange', 'Frame Range', '%s-%s' % + (nuke.root().firstFrame(), nuke.root().lastFrame())) + self.selected = nuke.Boolean_Knob("selection") + # ADD KNOBS + self.addKnob(self.selected) + self.addKnob(self.range) + self.selected.setValue(False) + + def knobChanged(self, knob): + frame_range = self.range.value() + pattern = r"^(?P-?[0-9]+)(?:(?:-+)(?P-?[0-9]+))?$" + match = re.match(pattern, frame_range) + frame_start = int(match.group("start")) + frame_end = int(match.group("end")) + if not self.read_node: + return + for r in self.read_node: + if self.onchecked(): + if not nuke.selectedNodes(): + return + if r in nuke.selectedNodes(): + r["frame_mode"].setValue("start_at") + r["frame"].setValue(frame_range) + r["first"].setValue(frame_start) + r["last"].setValue(frame_end) + else: + r["frame_mode"].setValue("start_at") + r["frame"].setValue(frame_range) + r["first"].setValue(frame_start) + r["last"].setValue(frame_end) + + def onchecked(self): + if self.selected.value(): + return True + else: + return False diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 35e5b1975c..cb06ad0a3b 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -227,7 +227,7 @@ "type": "action", "sourcetype": "python", "title": "Set Frame Range (Read Node)", - "command": "import openpype.hosts.nuke.startup.custom_popup as popup;popup.CustomScriptDialog(parent=popup.get_main_window()).show();", + "command": "import ops_frame_setting_for_read as popup;import nuke;popup.FrameSettingsPanel(nuke.allNodes('Read')).showModalDialog();", "tooltip": "Set Frame Range for Read Node(s)" } ] From 0655a6222bf3c69f4b4b3e9008c6c7803ffd3f94 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 1 Jun 2023 00:12:22 +0800 Subject: [PATCH 139/147] hound fix --- openpype/hosts/nuke/startup/ops_frame_setting_for_read.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/startup/ops_frame_setting_for_read.py b/openpype/hosts/nuke/startup/ops_frame_setting_for_read.py index 153effc7ad..bf98ef83f6 100644 --- a/openpype/hosts/nuke/startup/ops_frame_setting_for_read.py +++ b/openpype/hosts/nuke/startup/ops_frame_setting_for_read.py @@ -9,11 +9,14 @@ class FrameSettingsPanel(nukescripts.PythonPanel): self.read_node = node # CREATE KNOBS self.range = nuke.String_Knob('fRange', 'Frame Range', '%s-%s' % - (nuke.root().firstFrame(), nuke.root().lastFrame())) + (nuke.root().firstFrame(), + nuke.root().lastFrame())) self.selected = nuke.Boolean_Knob("selection") + self.info = nuke.Help_Knob("Instruction") # ADD KNOBS self.addKnob(self.selected) self.addKnob(self.range) + self.addKnob(self.info) self.selected.setValue(False) def knobChanged(self, knob): From aeaad018c1af7b30c9d1377d27ed91bec65e91cd Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 31 May 2023 18:12:48 +0200 Subject: [PATCH 140/147] :rotating_light: make peace with the hound :dog: --- openpype/hosts/max/api/lib_renderproducts.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 81057db733..94b0aeb913 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -15,14 +15,12 @@ class RenderProducts(object): def __init__(self, project_settings=None): self._project_settings = project_settings or get_project_settings( - legacy_io.Session["AVALON_PROJECT"] - ) + legacy_io.Session["AVALON_PROJECT"]) def get_beauty(self, container): render_dir = os.path.dirname(rt.rendOutputFilename) - output_file = os.path.join(render_dir, - container) + output_file = os.path.join(render_dir, container) setting = self._project_settings img_fmt = setting["max"]["RenderSettings"]["image_format"] # noqa From 021ea8d6380e439ccddcdbf3ac78542b4329a7e1 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 1 Jun 2023 00:30:19 +0800 Subject: [PATCH 141/147] update the command --- openpype/settings/defaults/project_settings/nuke.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index cb06ad0a3b..a0caa40396 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -227,7 +227,7 @@ "type": "action", "sourcetype": "python", "title": "Set Frame Range (Read Node)", - "command": "import ops_frame_setting_for_read as popup;import nuke;popup.FrameSettingsPanel(nuke.allNodes('Read')).showModalDialog();", + "command": "import openpype.hosts.nuke.startup.ops_frame_setting_for_read as popup;import nuke;popup.FrameSettingsPanel(nuke.allNodes('Read')).showModalDialog();", "tooltip": "Set Frame Range for Read Node(s)" } ] From 5379679f869db99bf169263ca43bb6f068df9dbf Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 1 Jun 2023 07:23:26 +0000 Subject: [PATCH 142/147] update README.md [skip ci] --- README.md | 75 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 39 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 514ffb62c0..8757e3db92 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -[![All Contributors](https://img.shields.io/badge/all_contributors-27-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-28-orange.svg?style=flat-square)](#contributors-) OpenPype ==== @@ -303,41 +303,44 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Milan Kolar

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

Jakub JeΕΎek

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

OndΕ™ej Samohel

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

Jakub Trllo

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

Petr Kalis

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

64qam

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

Roy Nieterau

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

Toke Jepsen

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

Jiri Sindelar

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

Simone Barbieri

πŸ’» πŸ“–

karimmozilla

πŸ’»

Allan I. A.

πŸ’»

murphy

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

Wijnand Koreman

πŸ’»

Bo Zhou

πŸ’»

ClΓ©ment Hector

πŸ’» πŸ‘€

David Lai

πŸ’» πŸ‘€

Derek

πŸ’» πŸ“–

GΓ‘bor Marinov

πŸ’» πŸ“–

icyvapor

πŸ’» πŸ“–

JΓ©rΓ΄me LORRAIN

πŸ’»

David Morris-Oliveros

πŸ’»

BenoitConnan

πŸ’»

Malthaldar

πŸ’»

Sven Neve

πŸ’»

zafrs

πŸ’»

FΓ©lix David

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

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

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

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

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

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

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

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

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

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

πŸ’» πŸ“–
karimmozilla
karimmozilla

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

πŸ’»
murphy
murphy

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

πŸ’»
Bo Zhou
Bo Zhou

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

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

πŸ’» πŸ‘€
Derek
Derek

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

πŸ’» πŸ“–
icyvapor
icyvapor

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

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

πŸ’»
BenoitConnan
BenoitConnan

πŸ’»
Malthaldar
Malthaldar

πŸ’»
Sven Neve
Sven Neve

πŸ’»
zafrs
zafrs

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

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

πŸ’»
From 28e9da8918b86d6ef30f2ffb549483edef3e7a24 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 1 Jun 2023 07:23:27 +0000 Subject: [PATCH 143/147] update .all-contributorsrc [skip ci] --- .all-contributorsrc | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index b30f3b2499..60812cdb3c 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1,6 +1,6 @@ { "projectName": "OpenPype", - "projectOwner": "pypeclub", + "projectOwner": "ynput", "repoType": "github", "repoHost": "https://github.com", "files": [ @@ -319,8 +319,18 @@ "code", "doc" ] + }, + { + "login": "movalex", + "name": "Alexey Bogomolov", + "avatar_url": "https://avatars.githubusercontent.com/u/11698866?v=4", + "profile": "http://abogomolov.com", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, - "skipCi": true + "skipCi": true, + "commitType": "docs" } From d4a807194ebb0971a629f608f3fc2ecf84723394 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 1 Jun 2023 11:24:34 +0200 Subject: [PATCH 144/147] Resolve: Make sure scripts dir exists (#5078) * make sure scripts dir exists * use exist_ok in makedirs --- openpype/hosts/resolve/utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/resolve/utils.py b/openpype/hosts/resolve/utils.py index 9a161f4865..1213fd9e7a 100644 --- a/openpype/hosts/resolve/utils.py +++ b/openpype/hosts/resolve/utils.py @@ -29,6 +29,9 @@ def setup(env): log.info("Utility Scripts Dir: `{}`".format(util_scripts_paths)) log.info("Utility Scripts: `{}`".format(scripts)) + # Make sure scripts dir exists + os.makedirs(util_scripts_dir, exist_ok=True) + # make sure no script file is in folder for script in os.listdir(util_scripts_dir): path = os.path.join(util_scripts_dir, script) From abc266e9e5868277b47c2dabdd1697ec13d1a024 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 1 Jun 2023 16:14:29 +0200 Subject: [PATCH 145/147] refactor the script to be frame number rather then frame range --- .../startup/frame_setting_for_read_nodes.py | 47 ++++++++++++++++++ .../startup/ops_frame_setting_for_read.py | 49 ------------------- .../defaults/project_settings/nuke.json | 6 +-- 3 files changed, 50 insertions(+), 52 deletions(-) create mode 100644 openpype/hosts/nuke/startup/frame_setting_for_read_nodes.py delete mode 100644 openpype/hosts/nuke/startup/ops_frame_setting_for_read.py diff --git a/openpype/hosts/nuke/startup/frame_setting_for_read_nodes.py b/openpype/hosts/nuke/startup/frame_setting_for_read_nodes.py new file mode 100644 index 0000000000..f0cbabe20f --- /dev/null +++ b/openpype/hosts/nuke/startup/frame_setting_for_read_nodes.py @@ -0,0 +1,47 @@ +""" OpenPype custom script for resetting read nodes start frame values """ + +import nuke +import nukescripts + + +class FrameSettingsPanel(nukescripts.PythonPanel): + """ Frame Settings Panel """ + def __init__(self): + nukescripts.PythonPanel.__init__(self, "Set Frame Start (Read Node)") + + # create knobs + self.frame = nuke.Int_Knob( + 'frame', 'Frame Number') + self.selected = nuke.Boolean_Knob("selection") + # add knobs to panel + self.addKnob(self.selected) + self.addKnob(self.frame) + + # set values + self.selected.setValue(False) + self.frame.setValue(nuke.root().firstFrame()) + + def process(self): + """ Process the panel values. """ + # get values + frame = self.frame.value() + if self.selected.value(): + # selected nodes processing + if not nuke.selectedNodes(): + return + for rn_ in nuke.selectedNodes(): + if rn_.Class() != "Read": + continue + rn_["frame_mode"].setValue("start_at") + rn_["frame"].setValue(str(frame)) + else: + # all nodes processing + for rn_ in nuke.allNodes(filter="Read"): + rn_["frame_mode"].setValue("start_at") + rn_["frame"].setValue(str(frame)) + + +def main(): + p_ = FrameSettingsPanel() + if p_.showModalDialog(): + print(p_.process()) diff --git a/openpype/hosts/nuke/startup/ops_frame_setting_for_read.py b/openpype/hosts/nuke/startup/ops_frame_setting_for_read.py deleted file mode 100644 index bf98ef83f6..0000000000 --- a/openpype/hosts/nuke/startup/ops_frame_setting_for_read.py +++ /dev/null @@ -1,49 +0,0 @@ -import nuke -import nukescripts -import re - - -class FrameSettingsPanel(nukescripts.PythonPanel): - def __init__(self, node): - nukescripts.PythonPanel.__init__(self, 'Frame Range') - self.read_node = node - # CREATE KNOBS - self.range = nuke.String_Knob('fRange', 'Frame Range', '%s-%s' % - (nuke.root().firstFrame(), - nuke.root().lastFrame())) - self.selected = nuke.Boolean_Knob("selection") - self.info = nuke.Help_Knob("Instruction") - # ADD KNOBS - self.addKnob(self.selected) - self.addKnob(self.range) - self.addKnob(self.info) - self.selected.setValue(False) - - def knobChanged(self, knob): - frame_range = self.range.value() - pattern = r"^(?P-?[0-9]+)(?:(?:-+)(?P-?[0-9]+))?$" - match = re.match(pattern, frame_range) - frame_start = int(match.group("start")) - frame_end = int(match.group("end")) - if not self.read_node: - return - for r in self.read_node: - if self.onchecked(): - if not nuke.selectedNodes(): - return - if r in nuke.selectedNodes(): - r["frame_mode"].setValue("start_at") - r["frame"].setValue(frame_range) - r["first"].setValue(frame_start) - r["last"].setValue(frame_end) - else: - r["frame_mode"].setValue("start_at") - r["frame"].setValue(frame_range) - r["first"].setValue(frame_start) - r["last"].setValue(frame_end) - - def onchecked(self): - if self.selected.value(): - return True - else: - return False diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index a0caa40396..3f8be4c872 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -226,9 +226,9 @@ { "type": "action", "sourcetype": "python", - "title": "Set Frame Range (Read Node)", - "command": "import openpype.hosts.nuke.startup.ops_frame_setting_for_read as popup;import nuke;popup.FrameSettingsPanel(nuke.allNodes('Read')).showModalDialog();", - "tooltip": "Set Frame Range for Read Node(s)" + "title": "Set Frame Start (Read Node)", + "command": "from openpype.hosts.nuke.startup.frame_setting_for_read_nodes import main;main();", + "tooltip": "Set frame start for read node(s)" } ] }, From 8356dfac7e1150cbe31cc8a63f26cee0c0fe1dd0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 1 Jun 2023 16:41:13 +0200 Subject: [PATCH 146/147] py2 compatibility --- openpype/hosts/nuke/startup/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 openpype/hosts/nuke/startup/__init__.py diff --git a/openpype/hosts/nuke/startup/__init__.py b/openpype/hosts/nuke/startup/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From d0886e43fe8efc8b675d9da5ccc9c37c459408d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Thu, 1 Jun 2023 16:42:42 +0200 Subject: [PATCH 147/147] fix doc --- website/docs/dev_blender.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/docs/dev_blender.md b/website/docs/dev_blender.md index 228447fb64..bed0e4a09d 100644 --- a/website/docs/dev_blender.md +++ b/website/docs/dev_blender.md @@ -9,14 +9,14 @@ toc_max_heading_level: 4 In case you need to execute a python script when Blender is started (aka [`-P`](https://docs.blender.org/manual/en/latest/advanced/command_line/arguments.html#python-options)), for example to programmatically modify a blender file for conformation, you can create an OpenPype hook as follows: ```python -from openpype.hosts.blender.hooks.pre_add_run_python_script_arg import AddPythonScriptToLaunchArgs +from openpype.hosts.blender.hooks import pre_add_run_python_script_arg from openpype.lib import PreLaunchHook class MyHook(PreLaunchHook): """Add python script to be executed before Blender launch.""" - order = AddPythonScriptToLaunchArgs.order - 1 + order = pre_add_run_python_script_arg.AddPythonScriptToLaunchArgs.order - 1 app_groups = [ "blender", ]