From d1364d2c15de6b66dcd985f5f514c669352c2179 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 13 Mar 2024 20:31:14 +0800 Subject: [PATCH 01/56] bug fix on getting aov expected files when global aov mode disabled --- .../hosts/maya/api/lib_renderproducts.py | 112 +++++++++--------- 1 file changed, 58 insertions(+), 54 deletions(-) diff --git a/client/ayon_core/hosts/maya/api/lib_renderproducts.py b/client/ayon_core/hosts/maya/api/lib_renderproducts.py index 7f26145e1d..63071c0053 100644 --- a/client/ayon_core/hosts/maya/api/lib_renderproducts.py +++ b/client/ayon_core/hosts/maya/api/lib_renderproducts.py @@ -1136,68 +1136,72 @@ class RenderProductsRedshift(ARenderProducts): light_groups_enabled = False has_beauty_aov = False colorspace = lib.get_color_management_output_transform() - for aov in aovs: - enabled = self._get_attr(aov, "enabled") - if not enabled: - continue + global_aov_enabled = bool( + self._get_attr("redshiftOptions.aovGlobalEnableMode") + ) + if global_aov_enabled: + for aov in aovs: + enabled = self._get_attr(aov, "enabled") + if not enabled: + continue - aov_type = self._get_attr(aov, "aovType") - if self.multipart and aov_type not in self.unmerged_aovs: - continue + aov_type = self._get_attr(aov, "aovType") + if self.multipart and aov_type not in self.unmerged_aovs: + continue - # Any AOVs that still get processed, like Cryptomatte - # by themselves are not multipart files. + # Any AOVs that still get processed, like Cryptomatte + # by themselves are not multipart files. - # Redshift skips rendering of masterlayer without AOV suffix - # when a Beauty AOV is rendered. It overrides the main layer. - if aov_type == "Beauty": - has_beauty_aov = True + # Redshift skips rendering of masterlayer without AOV suffix + # when a Beauty AOV is rendered. It overrides the main layer. + if aov_type == "Beauty": + has_beauty_aov = True - aov_name = self._get_attr(aov, "name") + aov_name = self._get_attr(aov, "name") - # Support light Groups - light_groups = [] - if self._get_attr(aov, "supportsLightGroups"): - all_light_groups = self._get_attr(aov, "allLightGroups") - if all_light_groups: - # All light groups is enabled - light_groups = self._get_redshift_light_groups() - else: - value = self._get_attr(aov, "lightGroupList") - # note: string value can return None when never set - if value: - selected_light_groups = value.strip().split() - light_groups = selected_light_groups + # Support light Groups + light_groups = [] + if self._get_attr(aov, "supportsLightGroups"): + all_light_groups = self._get_attr(aov, "allLightGroups") + if all_light_groups: + # All light groups is enabled + light_groups = self._get_redshift_light_groups() + else: + value = self._get_attr(aov, "lightGroupList") + # note: string value can return None when never set + if value: + selected_light_groups = value.strip().split() + light_groups = selected_light_groups - for light_group in light_groups: - aov_light_group_name = "{}_{}".format(aov_name, - light_group) - for camera in cameras: - product = RenderProduct( - productName=aov_light_group_name, - aov=aov_name, - ext=ext, - multipart=False, - camera=camera, - driver=aov, - colorspace=colorspace) - products.append(product) + for light_group in light_groups: + aov_light_group_name = "{}_{}".format(aov_name, + light_group) + for camera in cameras: + product = RenderProduct( + productName=aov_light_group_name, + aov=aov_name, + ext=ext, + multipart=False, + camera=camera, + driver=aov, + colorspace=colorspace) + products.append(product) - if light_groups: - light_groups_enabled = True + if light_groups: + light_groups_enabled = True - # Redshift AOV Light Select always renders the global AOV - # even when light groups are present so we don't need to - # exclude it when light groups are active - for camera in cameras: - product = RenderProduct(productName=aov_name, - aov=aov_name, - ext=ext, - multipart=False, - camera=camera, - driver=aov, - colorspace=colorspace) - products.append(product) + # Redshift AOV Light Select always renders the global AOV + # even when light groups are present so we don't need to + # exclude it when light groups are active + for camera in cameras: + product = RenderProduct(productName=aov_name, + aov=aov_name, + ext=ext, + multipart=False, + camera=camera, + driver=aov, + colorspace=colorspace) + products.append(product) # When a Beauty AOV is added manually, it will be rendered as # 'Beauty_other' in file name and "standard" beauty will have From 024f6f5163a72722f7bed1d2062c45cf02332d80 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 13 Mar 2024 21:22:23 +0800 Subject: [PATCH 02/56] return products instead if global aov mode is disabled --- .../hosts/maya/api/lib_renderproducts.py | 112 +++++++++--------- 1 file changed, 57 insertions(+), 55 deletions(-) diff --git a/client/ayon_core/hosts/maya/api/lib_renderproducts.py b/client/ayon_core/hosts/maya/api/lib_renderproducts.py index 63071c0053..28981e1187 100644 --- a/client/ayon_core/hosts/maya/api/lib_renderproducts.py +++ b/client/ayon_core/hosts/maya/api/lib_renderproducts.py @@ -1139,69 +1139,71 @@ class RenderProductsRedshift(ARenderProducts): global_aov_enabled = bool( self._get_attr("redshiftOptions.aovGlobalEnableMode") ) - if global_aov_enabled: - for aov in aovs: - enabled = self._get_attr(aov, "enabled") - if not enabled: - continue + if not global_aov_enabled: + return products - aov_type = self._get_attr(aov, "aovType") - if self.multipart and aov_type not in self.unmerged_aovs: - continue + for aov in aovs: + enabled = self._get_attr(aov, "enabled") + if not enabled: + continue - # Any AOVs that still get processed, like Cryptomatte - # by themselves are not multipart files. + aov_type = self._get_attr(aov, "aovType") + if self.multipart and aov_type not in self.unmerged_aovs: + continue - # Redshift skips rendering of masterlayer without AOV suffix - # when a Beauty AOV is rendered. It overrides the main layer. - if aov_type == "Beauty": - has_beauty_aov = True + # Any AOVs that still get processed, like Cryptomatte + # by themselves are not multipart files. - aov_name = self._get_attr(aov, "name") + # Redshift skips rendering of masterlayer without AOV suffix + # when a Beauty AOV is rendered. It overrides the main layer. + if aov_type == "Beauty": + has_beauty_aov = True - # Support light Groups - light_groups = [] - if self._get_attr(aov, "supportsLightGroups"): - all_light_groups = self._get_attr(aov, "allLightGroups") - if all_light_groups: - # All light groups is enabled - light_groups = self._get_redshift_light_groups() - else: - value = self._get_attr(aov, "lightGroupList") - # note: string value can return None when never set - if value: - selected_light_groups = value.strip().split() - light_groups = selected_light_groups + aov_name = self._get_attr(aov, "name") - for light_group in light_groups: - aov_light_group_name = "{}_{}".format(aov_name, - light_group) - for camera in cameras: - product = RenderProduct( - productName=aov_light_group_name, - aov=aov_name, - ext=ext, - multipart=False, - camera=camera, - driver=aov, - colorspace=colorspace) - products.append(product) + # Support light Groups + light_groups = [] + if self._get_attr(aov, "supportsLightGroups"): + all_light_groups = self._get_attr(aov, "allLightGroups") + if all_light_groups: + # All light groups is enabled + light_groups = self._get_redshift_light_groups() + else: + value = self._get_attr(aov, "lightGroupList") + # note: string value can return None when never set + if value: + selected_light_groups = value.strip().split() + light_groups = selected_light_groups - if light_groups: - light_groups_enabled = True + for light_group in light_groups: + aov_light_group_name = "{}_{}".format(aov_name, + light_group) + for camera in cameras: + product = RenderProduct( + productName=aov_light_group_name, + aov=aov_name, + ext=ext, + multipart=False, + camera=camera, + driver=aov, + colorspace=colorspace) + products.append(product) - # Redshift AOV Light Select always renders the global AOV - # even when light groups are present so we don't need to - # exclude it when light groups are active - for camera in cameras: - product = RenderProduct(productName=aov_name, - aov=aov_name, - ext=ext, - multipart=False, - camera=camera, - driver=aov, - colorspace=colorspace) - products.append(product) + if light_groups: + light_groups_enabled = True + + # Redshift AOV Light Select always renders the global AOV + # even when light groups are present so we don't need to + # exclude it when light groups are active + for camera in cameras: + product = RenderProduct(productName=aov_name, + aov=aov_name, + ext=ext, + multipart=False, + camera=camera, + driver=aov, + colorspace=colorspace) + products.append(product) # When a Beauty AOV is added manually, it will be rendered as # 'Beauty_other' in file name and "standard" beauty will have From b729bf4f30432a5a86ab7670e73b3e2b4514cc87 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 13 Mar 2024 23:56:31 +0800 Subject: [PATCH 03/56] make sure beauty still being added as render product --- client/ayon_core/hosts/maya/api/lib_renderproducts.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/client/ayon_core/hosts/maya/api/lib_renderproducts.py b/client/ayon_core/hosts/maya/api/lib_renderproducts.py index 28981e1187..3bba25ee12 100644 --- a/client/ayon_core/hosts/maya/api/lib_renderproducts.py +++ b/client/ayon_core/hosts/maya/api/lib_renderproducts.py @@ -1140,6 +1140,15 @@ class RenderProductsRedshift(ARenderProducts): self._get_attr("redshiftOptions.aovGlobalEnableMode") ) if not global_aov_enabled: + beauty_name = "BeautyAux" if has_beauty_aov else "" + for camera in cameras: + products.insert(0, + RenderProduct(productName=beauty_name, + ext=ext, + multipart=self.multipart, + camera=camera, + colorspace=colorspace)) + return products for aov in aovs: From ffa395af64b066808cdffce100ad8593bf36fbd1 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 14 Mar 2024 15:10:03 +0800 Subject: [PATCH 04/56] add only beauty output when global aov disabled --- .../hosts/maya/api/lib_renderproducts.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/client/ayon_core/hosts/maya/api/lib_renderproducts.py b/client/ayon_core/hosts/maya/api/lib_renderproducts.py index 3bba25ee12..5f3917f642 100644 --- a/client/ayon_core/hosts/maya/api/lib_renderproducts.py +++ b/client/ayon_core/hosts/maya/api/lib_renderproducts.py @@ -1133,24 +1133,24 @@ class RenderProductsRedshift(ARenderProducts): aovs = list(set(aovs) - set(ref_aovs)) products = [] - light_groups_enabled = False - has_beauty_aov = False - colorspace = lib.get_color_management_output_transform() global_aov_enabled = bool( self._get_attr("redshiftOptions.aovGlobalEnableMode") ) + colorspace = lib.get_color_management_output_transform() if not global_aov_enabled: - beauty_name = "BeautyAux" if has_beauty_aov else "" + # only beauty output for camera in cameras: products.insert(0, - RenderProduct(productName=beauty_name, - ext=ext, - multipart=self.multipart, - camera=camera, - colorspace=colorspace)) - + RenderProduct(productName="", + ext=ext, + multipart=self.multipart, + camera=camera, + colorspace=colorspace)) return products + light_groups_enabled = False + has_beauty_aov = False + for aov in aovs: enabled = self._get_attr(aov, "enabled") if not enabled: From 85dc52a2c1b9c63d29c9dd1755c2fe316cf5e2cd Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 20 Mar 2024 17:30:34 +0100 Subject: [PATCH 05/56] Fusion: Add Launch Hook to start "AYON menu" on Fusion start --- .../hooks/pre_fusion_launch_menu_hook.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 client/ayon_core/hosts/fusion/hooks/pre_fusion_launch_menu_hook.py diff --git a/client/ayon_core/hosts/fusion/hooks/pre_fusion_launch_menu_hook.py b/client/ayon_core/hosts/fusion/hooks/pre_fusion_launch_menu_hook.py new file mode 100644 index 0000000000..825d432a3e --- /dev/null +++ b/client/ayon_core/hosts/fusion/hooks/pre_fusion_launch_menu_hook.py @@ -0,0 +1,34 @@ +import os +from ayon_core.lib import PreLaunchHook +from ayon_core.hosts.fusion import FUSION_HOST_DIR + + +class FusionLaunchMenuHook(PreLaunchHook): + """Launch OpenPype menu on start of Fusion""" + app_groups = ["fusion"] + order = 9 + + def execute(self): + + # TODO: Make this optional via project settings + + variant = self.application.name + if variant.isnumeric(): + version = int(variant) + if version < 18: + print("Skipping launch of OpenPype menu on Fusion start " + "because Fusion version below 18.0 does not support " + "/execute argument on launch. " + f"Version detected: {version}") + return + else: + print(f"Application variant is not numeric: {variant}. " + "Validation for Fusion version 18+ for /execute " + "prelaunch argument skipped.") + + path = os.path.join(FUSION_HOST_DIR, + "deploy", + "MenuScripts", + "launch_menu.py").replace("\\", "/") + script = f"fusion:RunScript('{path}')" + self.launch_context.launch_args.extend(["/execute", script]) From 39399b1b269d63e4f0cf1cc9390a193c7e87cdc3 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 21 Mar 2024 16:32:37 +0000 Subject: [PATCH 06/56] Publish and load slate frame explicitly --- .../hosts/nuke/plugins/load/load_clip.py | 34 +++++++++++-------- .../plugins/publish/extract_slate_data.py | 22 ++++++++++++ 2 files changed, 41 insertions(+), 15 deletions(-) create mode 100644 client/ayon_core/plugins/publish/extract_slate_data.py diff --git a/client/ayon_core/hosts/nuke/plugins/load/load_clip.py b/client/ayon_core/hosts/nuke/plugins/load/load_clip.py index 8a41d854d9..b2044901a3 100644 --- a/client/ayon_core/hosts/nuke/plugins/load/load_clip.py +++ b/client/ayon_core/hosts/nuke/plugins/load/load_clip.py @@ -130,6 +130,11 @@ class LoadClip(plugin.NukeLoader): first = 1 last = first + duration + # If a slate is present, the frame range is 1 frame longer. + slate_frame = repre_entity["data"].get("slateFrame", False) + if slate_frame: + last += 1 + # Fallback to folder name when namespace is None if namespace is None: namespace = context["folder"]["name"] @@ -167,7 +172,9 @@ class LoadClip(plugin.NukeLoader): repre_entity ) - self._set_range_to_node(read_node, first, last, start_at_workfile) + self._set_range_to_node( + read_node, first, last, start_at_workfile, slate_frame + ) version_name = version_entity["version"] if version_name < 0: @@ -402,14 +409,23 @@ class LoadClip(plugin.NukeLoader): for member in members: nuke.delete(member) - def _set_range_to_node(self, read_node, first, last, start_at_workfile): + def _set_range_to_node( + self, read_node, first, last, start_at_workfile, slate_frame=False + ): read_node['origfirst'].setValue(int(first)) read_node['first'].setValue(int(first)) read_node['origlast'].setValue(int(last)) read_node['last'].setValue(int(last)) # set start frame depending on workfile or version - self._loader_shift(read_node, start_at_workfile) + if start_at_workfile: + read_node['frame_mode'].setValue("start at") + + start_frame = self.script_start + if slate_frame: + start_frame -= 1 + + read_node['frame'].setValue(str(start_frame)) def _make_retimes(self, parent_node, version_data): ''' Create all retime and timewarping nodes with copied animation ''' @@ -466,18 +482,6 @@ class LoadClip(plugin.NukeLoader): for i, n in enumerate(dependent_nodes): last_node.setInput(i, n) - def _loader_shift(self, read_node, workfile_start=False): - """ Set start frame of read node to a workfile start - - Args: - read_node (nuke.Node): The nuke's read node - workfile_start (bool): set workfile start frame if true - - """ - if workfile_start: - read_node['frame_mode'].setValue("start at") - read_node['frame'].setValue(str(self.script_start)) - def _get_node_name(self, context): folder_entity = context["folder"] product_name = context["product"]["name"] diff --git a/client/ayon_core/plugins/publish/extract_slate_data.py b/client/ayon_core/plugins/publish/extract_slate_data.py new file mode 100644 index 0000000000..0a28083f1e --- /dev/null +++ b/client/ayon_core/plugins/publish/extract_slate_data.py @@ -0,0 +1,22 @@ +import pyblish.api + +from ayon_core.pipeline import publish + + +class ExtractSlateData(publish.Extractor): + """Add slate data for integration.""" + + label = "Slate Data" + # Offset from ExtractReviewSlate and ExtractGenerateSlate. + order = pyblish.api.ExtractorOrder + 0.49 + families = ["slate", "review"] + hosts = ["nuke", "shell"] + + def process(self, instance): + for representation in instance.data.get("representations", []): + if "slate-frame" not in representation.get("tags", []): + continue + + data = representation.get("data", {}) + data["slateFrame"] = True + representation["data"] = data From d1e1f3942a7ed9cd88628b774f7421f14b07dd33 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 21 Mar 2024 17:38:43 +0000 Subject: [PATCH 07/56] Fix file sequences --- .../ayon_core/hosts/nuke/plugins/load/load_clip.py | 12 ++++++++++-- .../nuke/plugins/publish/extract_slate_frame.py | 4 ++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/hosts/nuke/plugins/load/load_clip.py b/client/ayon_core/hosts/nuke/plugins/load/load_clip.py index b2044901a3..86a6d359c3 100644 --- a/client/ayon_core/hosts/nuke/plugins/load/load_clip.py +++ b/client/ayon_core/hosts/nuke/plugins/load/load_clip.py @@ -130,10 +130,18 @@ class LoadClip(plugin.NukeLoader): first = 1 last = first + duration - # If a slate is present, the frame range is 1 frame longer. + # If a slate is present, the frame range is 1 frame longer for movies, + # but file sequences its the first frame that is 1 frame lower. slate_frame = repre_entity["data"].get("slateFrame", False) if slate_frame: - last += 1 + extension = "." + repre_entity["context"]["ext"] + + if extension in VIDEO_EXTENSIONS: + last += 1 + + files_count = len(repre_entity["files"]) + if extension in IMAGE_EXTENSIONS and files_count != 1: + first -= 1 # Fallback to folder name when namespace is None if namespace is None: diff --git a/client/ayon_core/hosts/nuke/plugins/publish/extract_slate_frame.py b/client/ayon_core/hosts/nuke/plugins/publish/extract_slate_frame.py index c013da84d2..67d2bd272c 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/extract_slate_frame.py +++ b/client/ayon_core/hosts/nuke/plugins/publish/extract_slate_frame.py @@ -300,6 +300,10 @@ class ExtractSlateFrame(publish.Extractor): self.log.debug( "__ matching_repre: {}".format(pformat(matching_repre))) + data = matching_repre.get("data", {}) + data["slateFrame"] = True + matching_repre["data"] = data + self.log.info("Added slate frame to representation files") def add_comment_slate_node(self, instance, node): From 943b4d7b087b9d0e257013b5be2e65d4c70d7da7 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 25 Mar 2024 13:35:25 +0100 Subject: [PATCH 08/56] Also update tool on only task or asset change --- client/ayon_core/hosts/fusion/api/plugin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/ayon_core/hosts/fusion/api/plugin.py b/client/ayon_core/hosts/fusion/api/plugin.py index f63b5eaec3..1af3467bf1 100644 --- a/client/ayon_core/hosts/fusion/api/plugin.py +++ b/client/ayon_core/hosts/fusion/api/plugin.py @@ -125,6 +125,8 @@ class GenericCreateSaver(Creator): product_name = data["productName"] if ( original_product_name != product_name + or tool.GetData("openpype.task") != data["task"] + or tool.GetData("openpype.asset") != data["asset"] or original_format != data["creator_attributes"]["image_format"] ): self._configure_saver_tool(data, tool, product_name) From 55b389b7a7707dd1464bb819a440f2484c52a46b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 25 Mar 2024 21:36:53 +0100 Subject: [PATCH 09/56] Only launch when enabled in settings --- .../hosts/fusion/hooks/pre_fusion_launch_menu_hook.py | 6 ++++-- server_addon/fusion/server/settings.py | 9 +++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/hosts/fusion/hooks/pre_fusion_launch_menu_hook.py b/client/ayon_core/hosts/fusion/hooks/pre_fusion_launch_menu_hook.py index 825d432a3e..f5a1c75a50 100644 --- a/client/ayon_core/hosts/fusion/hooks/pre_fusion_launch_menu_hook.py +++ b/client/ayon_core/hosts/fusion/hooks/pre_fusion_launch_menu_hook.py @@ -9,8 +9,10 @@ class FusionLaunchMenuHook(PreLaunchHook): order = 9 def execute(self): - - # TODO: Make this optional via project settings + # Prelaunch hook is optional + settings = self.data["project_settings"][self.host_name] + if not settings["hooks"]["FusionLaunchMenuHook"]["enabled"]: + return variant = self.application.name if variant.isnumeric(): diff --git a/server_addon/fusion/server/settings.py b/server_addon/fusion/server/settings.py index a913db16da..b44feb1f03 100644 --- a/server_addon/fusion/server/settings.py +++ b/server_addon/fusion/server/settings.py @@ -75,6 +75,12 @@ class HooksModel(BaseSettingsModel): default_factory=HookOptionalModel, title="Install PySide2" ) + FusionLaunchMenuHook: HookOptionalModel = SettingsField( + default_factory=HookOptionalModel, + title="Launch AYON Menu on Fusion Start", + description="Launch the AYON menu on Fusion application startup. " + "This is only supported for Fusion 18+" + ) class CreateSaverModel(CreateSaverPluginModel): @@ -143,6 +149,9 @@ DEFAULT_VALUES = { "hooks": { "InstallPySideToFusion": { "enabled": True + }, + "FusionLaunchMenuHook": { + "enabled": True } }, "create": { From 38c32c55af7b882cb75b161cbf581bc7c6cc2881 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 25 Mar 2024 21:37:22 +0100 Subject: [PATCH 10/56] Bump fusion server addon version --- server_addon/fusion/server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/fusion/server/version.py b/server_addon/fusion/server/version.py index bbab0242f6..1276d0254f 100644 --- a/server_addon/fusion/server/version.py +++ b/server_addon/fusion/server/version.py @@ -1 +1 @@ -__version__ = "0.1.4" +__version__ = "0.1.5" From 2ae618d2cbef243e3ea9ab57197ceda813410aca Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 25 Mar 2024 21:51:12 +0100 Subject: [PATCH 11/56] Update client/ayon_core/hosts/fusion/hooks/pre_fusion_launch_menu_hook.py --- .../ayon_core/hosts/fusion/hooks/pre_fusion_launch_menu_hook.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/fusion/hooks/pre_fusion_launch_menu_hook.py b/client/ayon_core/hosts/fusion/hooks/pre_fusion_launch_menu_hook.py index f5a1c75a50..e70d4b844e 100644 --- a/client/ayon_core/hosts/fusion/hooks/pre_fusion_launch_menu_hook.py +++ b/client/ayon_core/hosts/fusion/hooks/pre_fusion_launch_menu_hook.py @@ -4,7 +4,7 @@ from ayon_core.hosts.fusion import FUSION_HOST_DIR class FusionLaunchMenuHook(PreLaunchHook): - """Launch OpenPype menu on start of Fusion""" + """Launch AYON menu on start of Fusion""" app_groups = ["fusion"] order = 9 From b17df02e418b2061ec6631d028e3039ce55105f5 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 26 Mar 2024 23:04:58 +0100 Subject: [PATCH 12/56] Maya: Validate Model Content improve validation message - Also fix `get_invalid` actually returning the instance node. --- .../plugins/publish/validate_model_content.py | 41 +++++++++++++------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_model_content.py b/client/ayon_core/hosts/maya/plugins/publish/validate_model_content.py index 37c78a72ee..97d602a9ad 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_model_content.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_model_content.py @@ -28,13 +28,15 @@ class ValidateModelContent(pyblish.api.InstancePlugin, validate_top_group = True optional = False + allowed = ('mesh', 'transform', 'nurbsCurve', 'nurbsSurface', 'locator') + @classmethod def get_invalid(cls, instance): content_instance = instance.data.get("setMembers", None) if not content_instance: cls.log.error("Instance has no nodes!") - return [instance.data["name"]] + return [instance.data["instance_node"]] # All children will be included in the extracted export so we also # validate *all* descendents of the set members and we skip any @@ -46,30 +48,39 @@ class ValidateModelContent(pyblish.api.InstancePlugin, content_instance = list(set(content_instance + descendants)) # Ensure only valid node types - allowed = ('mesh', 'transform', 'nurbsCurve', 'nurbsSurface', 'locator') nodes = cmds.ls(content_instance, long=True) - valid = cmds.ls(content_instance, long=True, type=allowed) + valid = cmds.ls(content_instance, long=True, type=cls.allowed) invalid = set(nodes) - set(valid) if invalid: - cls.log.error("These nodes are not allowed: %s" % invalid) + cls.log.error( + "These nodes are not allowed: {}.\n" + "The valid node types are: {}".format(", ".join(invalid), + ", ".join(cls.allowed)) + ) return list(invalid) if not valid: - cls.log.error("No valid nodes in the instance") - return True + cls.log.error( + "No valid nodes in the model instance.\n" + "The valid node types are: {}".format(", ".join(cls.allowed)) + ) + return [instance.data["instance_node"]] # Ensure it has shapes shapes = cmds.ls(valid, long=True, shapes=True) if not shapes: cls.log.error("No shapes in the model instance") - return True + return [instance.data["instance_node"]] - # Top group - top_parents = set([x.split("|")[1] for x in content_instance]) + # Ensure single top group + top_parents = {x.split("|", 2)[1] for x in content_instance} if cls.validate_top_group and len(top_parents) != 1: - cls.log.error("Must have exactly one top group") - return top_parents + cls.log.error( + "A model instance must have exactly one top group. " + "Found top groups: {}".format(", ".join(top_parents)) + ) + return list(top_parents) def _is_visible(node): """Return whether node is visible""" @@ -101,5 +112,11 @@ class ValidateModelContent(pyblish.api.InstancePlugin, if invalid: raise PublishValidationError( title="Model content is invalid", - message="See log for more details" + message="Model content is invalid. See log for more details.", + description=( + "## Model content is invalid\n" + "Your model instance does not adhere to the rules of a " + "model.\n\n" + "See log for more details." + ) ) From cb6f4dd79857ffbe62e8a99cb91d3db83f8712d5 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 27 Mar 2024 09:56:44 +0100 Subject: [PATCH 13/56] Turn off by default --- server_addon/fusion/server/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/fusion/server/settings.py b/server_addon/fusion/server/settings.py index b44feb1f03..f16ae6e3e7 100644 --- a/server_addon/fusion/server/settings.py +++ b/server_addon/fusion/server/settings.py @@ -151,7 +151,7 @@ DEFAULT_VALUES = { "enabled": True }, "FusionLaunchMenuHook": { - "enabled": True + "enabled": False } }, "create": { From 149c83e136d342b53bc667b114df43ede22915f3 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 28 Mar 2024 13:57:37 +0100 Subject: [PATCH 14/56] Raise PublishValidationError --- .../publish/validate_current_renderlayer_renderable.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_current_renderlayer_renderable.py b/client/ayon_core/hosts/maya/plugins/publish/validate_current_renderlayer_renderable.py index 4590c53931..332c5aff02 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_current_renderlayer_renderable.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_current_renderlayer_renderable.py @@ -3,9 +3,11 @@ import pyblish.api from maya import cmds from ayon_core.pipeline.publish import ( context_plugin_should_run, + PublishValidationError, OptionalPyblishPluginMixin ) + class ValidateCurrentRenderLayerIsRenderable(pyblish.api.ContextPlugin, OptionalPyblishPluginMixin): """Validate if current render layer has a renderable camera @@ -35,5 +37,9 @@ class ValidateCurrentRenderLayerIsRenderable(pyblish.api.ContextPlugin, layer = cmds.editRenderLayerGlobals(query=True, currentRenderLayer=True) cameras = cmds.ls(type="camera", long=True) renderable = any(c for c in cameras if cmds.getAttr(c + ".renderable")) - assert renderable, ("Current render layer '%s' has no renderable " - "camera" % layer) + if not renderable: + raise PublishValidationError( + "Current render layer '{}' has no renderable camera".format( + layer + ) + ) From 4a69d68a40c845180baa36e6c665d608d1fe1b6c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 28 Mar 2024 14:00:05 +0100 Subject: [PATCH 15/56] Add description to report --- .../publish/validate_current_renderlayer_renderable.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_current_renderlayer_renderable.py b/client/ayon_core/hosts/maya/plugins/publish/validate_current_renderlayer_renderable.py index 332c5aff02..f02c401bf9 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_current_renderlayer_renderable.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_current_renderlayer_renderable.py @@ -1,3 +1,5 @@ +import inspect + import pyblish.api from maya import cmds @@ -10,7 +12,7 @@ from ayon_core.pipeline.publish import ( class ValidateCurrentRenderLayerIsRenderable(pyblish.api.ContextPlugin, OptionalPyblishPluginMixin): - """Validate if current render layer has a renderable camera + """Validate if current render layer has a renderable camera. There is a bug in Redshift which occurs when the current render layer at file open has no renderable camera. The error raised is as follows: @@ -41,5 +43,6 @@ class ValidateCurrentRenderLayerIsRenderable(pyblish.api.ContextPlugin, raise PublishValidationError( "Current render layer '{}' has no renderable camera".format( layer - ) + ), + description=inspect.getdoc(self) ) From 03ee91cf2c2826e0ff865b2d0949c71d8ca7ee46 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 28 Mar 2024 14:01:51 +0100 Subject: [PATCH 16/56] Only query current renderlayer if there is no renderable camera --- .../plugins/publish/validate_current_renderlayer_renderable.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_current_renderlayer_renderable.py b/client/ayon_core/hosts/maya/plugins/publish/validate_current_renderlayer_renderable.py index f02c401bf9..da8468c1b6 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_current_renderlayer_renderable.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_current_renderlayer_renderable.py @@ -36,10 +36,11 @@ class ValidateCurrentRenderLayerIsRenderable(pyblish.api.ContextPlugin, if not context_plugin_should_run(self, context): return - layer = cmds.editRenderLayerGlobals(query=True, currentRenderLayer=True) cameras = cmds.ls(type="camera", long=True) renderable = any(c for c in cameras if cmds.getAttr(c + ".renderable")) if not renderable: + layer = cmds.editRenderLayerGlobals(query=True, + currentRenderLayer=True) raise PublishValidationError( "Current render layer '{}' has no renderable camera".format( layer From 312fd21b23f680c9f2032497ae9b857b38c52f99 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 28 Mar 2024 19:37:25 +0100 Subject: [PATCH 17/56] Expose `timecode` for formatting ffmpeg args --- .../plugins/publish/extract_review.py | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 905158c851..ac213b8669 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -32,6 +32,35 @@ from ayon_core.pipeline.publish import ( from ayon_core.pipeline.publish.lib import add_repre_files_for_cleanup +def frame_to_timecode(frame: int, fps: float) -> str: + """Convert a frame number and FPS to editorial timecode (HH:MM:SS:FF). + + Unlike `ayon_core.pipeline.editorial.frames_to_timecode` this does not + rely on the `opentimelineio` package, so it can be used across hosts that + do not have it available. + + Args: + frame (int): The frame number to be converted. + fps (float): The frames per second of the video. + + Returns: + str: The timecode in HH:MM:SS:FF format. + """ + # Calculate total seconds + total_seconds = frame / fps + + # Extract hours, minutes, and seconds + hours = int(total_seconds // 3600) + minutes = int((total_seconds % 3600) // 60) + seconds = int(total_seconds % 60) + + # Adjust for non-integer FPS by rounding the remaining frames appropriately + remaining_frames = round((total_seconds - int(total_seconds)) * fps) + + # Format and return the timecode + return f"{hours:02d}:{minutes:02d}:{seconds:02d}:{remaining_frames:02d}" + + class ExtractReview(pyblish.api.InstancePlugin): """Extracting Review mov file for Ftrack @@ -390,7 +419,16 @@ class ExtractReview(pyblish.api.InstancePlugin): # add outputName to anatomy format fill_data fill_data.update({ "output": output_name, - "ext": output_ext + "ext": output_ext, + + # By adding `timecode` as data we can use it + # in the ffmpeg arguments for `--timecode` so that editorial + # like Resolve or Premiere can detect the start frame for e.g. + # review output files + "timecode": frame_to_timecode( + frame=temp_data["frame_start_handle"], + fps=float(instance.data["fps"]) + ) }) try: # temporary until oiiotool is supported cross platform From 33fd95dd009d65097cc68f7f4f10251eff4ea874 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 29 Mar 2024 10:57:27 +0100 Subject: [PATCH 18/56] Add `usd` to product type enum for Loader Product Type Filter Used in `ayon+settings://core/tools/loader/product_type_filter_profiles` --- server/settings/tools.py | 1 + 1 file changed, 1 insertion(+) diff --git a/server/settings/tools.py b/server/settings/tools.py index 488d27e8f1..fb8430a71c 100644 --- a/server/settings/tools.py +++ b/server/settings/tools.py @@ -173,6 +173,7 @@ def _product_types_enum(): "rig", "setdress", "take", + "usd", "usdShade", "vdbcache", "vrayproxy", From a4b0dbc4936da61f167912167959916138b8e2b4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 29 Mar 2024 13:17:49 +0100 Subject: [PATCH 19/56] Optimize `cbId` logic for large scenes --- client/ayon_core/hosts/maya/api/lib.py | 191 +++++++++++++----- client/ayon_core/hosts/maya/api/pipeline.py | 3 +- .../maya/plugins/publish/validate_node_ids.py | 9 +- .../publish/validate_node_ids_in_database.py | 11 +- 4 files changed, 153 insertions(+), 61 deletions(-) diff --git a/client/ayon_core/hosts/maya/api/lib.py b/client/ayon_core/hosts/maya/api/lib.py index b18d3a0c33..4b9b80f0e7 100644 --- a/client/ayon_core/hosts/maya/api/lib.py +++ b/client/ayon_core/hosts/maya/api/lib.py @@ -1519,24 +1519,30 @@ def extract_alembic(file, # region ID -def get_id_required_nodes(referenced_nodes=False, nodes=None): - """Filter out any node which are locked (reference) or readOnly +def get_id_required_nodes(referenced_nodes=False, + nodes=None, + existing_ids=True): + """Return nodes that should receive a `cbId` attribute. + + This includes only mesh and curve nodes, parent transforms of the shape + nodes, file texture nodes and object sets (including shading engines). + + This filters out any node which is locked, referenced, read-only, + intermediate object. Args: - referenced_nodes (bool): set True to filter out reference nodes + referenced_nodes (bool): set True to include referenced nodes nodes (list, Optional): nodes to consider + existing_ids (bool): set True to include nodes with `cbId` attribute + Returns: nodes (set): list of filtered nodes """ - lookup = None - if nodes is None: - # Consider all nodes - nodes = cmds.ls() - else: - # Build a lookup for the only allowed nodes in output based - # on `nodes` input of the function (+ ensure long names) - lookup = set(cmds.ls(nodes, long=True)) + if nodes is not None and not nodes: + # User supplied an empty `nodes` list to check so all we can + # do is return the empty result + return set() def _node_type_exists(node_type): try: @@ -1545,63 +1551,142 @@ def get_id_required_nodes(referenced_nodes=False, nodes=None): except RuntimeError: return False + def iterate(maya_iterator): + while not maya_iterator.isDone(): + yield maya_iterator.thisNode() + maya_iterator.next() + # `readOnly` flag is obsolete as of Maya 2016 therefore we explicitly # remove default nodes and reference nodes - camera_shapes = ["frontShape", "sideShape", "topShape", "perspShape"] + default_camera_shapes = { + "frontShape", "sideShape", "topShape", "perspShape" + } - ignore = set() - if not referenced_nodes: - ignore |= set(cmds.ls(long=True, referencedNodes=True)) - - # list all defaultNodes to filter out from the rest - ignore |= set(cmds.ls(long=True, defaultNodes=True)) - ignore |= set(cmds.ls(camera_shapes, long=True)) - - # Remove Turtle from the result of `cmds.ls` if Turtle is loaded - # TODO: This should be a less specific check for a single plug-in. - if _node_type_exists("ilrBakeLayer"): - ignore |= set(cmds.ls(type="ilrBakeLayer", long=True)) - - # Establish set of nodes types to include - types = ["objectSet", "file", "mesh", "nurbsCurve", "nurbsSurface"] + # The filtered types do not include transforms because we only want the + # parent transforms that have a child shape that we filtered to, so we + # include the parents here + types = ["mesh", "nurbsCurve", "nurbsSurface", "file", "objectSet"] # Check if plugin nodes are available for Maya by checking if the plugin # is loaded if cmds.pluginInfo("pgYetiMaya", query=True, loaded=True): types.append("pgYetiMaya") - # We *always* ignore intermediate shapes, so we filter them out directly - nodes = cmds.ls(nodes, type=types, long=True, noIntermediate=True) + iterator_type = OpenMaya.MIteratorType() + # This tries to be closest matching API equivalents of `types` variable + iterator_type.filterList = [ + OpenMaya.MFn.kMesh, # mesh + OpenMaya.MFn.kNurbsSurface, # nurbsSurface + OpenMaya.MFn.kNurbsCurve, # nurbsCurve + OpenMaya.MFn.kFileTexture, # file + OpenMaya.MFn.kSet, # objectSet + OpenMaya.MFn.kPluginShape # pgYetiMaya + ] + it = OpenMaya.MItDependencyNodes(iterator_type) - # The items which need to pass the id to their parent - # Add the collected transform to the nodes - dag = cmds.ls(nodes, type="dagNode", long=True) # query only dag nodes - transforms = cmds.listRelatives(dag, - parent=True, - fullPath=True) or [] + fn_dep = OpenMaya.MFnDependencyNode() + fn_dag = OpenMaya.MFnDagNode() + result = set() - nodes = set(nodes) - nodes |= set(transforms) + def _should_include_parents(obj): + """Whether to include parents of obj in output""" + if not obj.hasFn(OpenMaya.MFn.kShape): + return False - nodes -= ignore # Remove the ignored nodes - if not nodes: - return nodes + fn_dag.setObject(obj) + if fn_dag.isIntermediateObject: + return False - # Ensure only nodes from the input `nodes` are returned when a - # filter was applied on function call because we also iterated - # to parents and alike - if lookup is not None: - nodes &= lookup + # Skip default cameras + if ( + obj.hasFn(OpenMaya.MFn.kCamera) and + fn_dag.name() in default_camera_shapes + ): + return False - # Avoid locked nodes - nodes_list = list(nodes) - locked = cmds.lockNode(nodes_list, query=True, lock=True) - for node, lock in zip(nodes_list, locked): - if lock: - log.warning("Skipping locked node: %s" % node) - nodes.remove(node) + return True - return nodes + def _add_to_result_if_valid(obj): + """Add to `result` if the object should be included""" + fn_dep.setObject(obj) + if not existing_ids and fn_dep.hasAttribute("cbId"): + return + + if not referenced_nodes and fn_dep.isFromReferencedFile: + return + + if fn_dep.isDefaultNode: + return + + if fn_dep.isLocked: + return + + # Skip default cameras + if ( + obj.hasFn(OpenMaya.MFn.kCamera) and + fn_dep.name() in default_camera_shapes + ): + return + + if obj.hasFn(OpenMaya.MFn.kDagNode): + # DAG nodes + fn_dag.setObject(obj) + + # Skip intermediate objects + if fn_dag.isIntermediateObject: + return + + # DAG nodes can be instanced and thus may have multiple paths. + # We need to identify each path + paths = OpenMaya.MDagPath.getAllPathsTo(obj) + for dag in paths: + path = dag.fullPathName() + result.add(path) + else: + # Dependency node + path = fn_dep.name() + result.add(path) + + for obj in iterate(it): + # For any non-intermediate shape node always include the parent + # even if we exclude the shape itself (e.g. when locked, default) + if _should_include_parents(obj): + fn_dag.setObject(obj) + parents = [ + fn_dag.parent(index) for index in range(fn_dag.parentCount()) + ] + for parent_obj in parents: + _add_to_result_if_valid(parent_obj) + + _add_to_result_if_valid(obj) + + if not result: + return result + + # Exclude some additional types + exclude_types = [] + if _node_type_exists("ilrBakeLayer"): + # Remove Turtle from the result if Turtle is loaded + exclude_types.append("ilrBakeLayer") + + if exclude_types: + exclude_nodes = set(cmds.ls(nodes, long=True, type=exclude_types)) + if exclude_nodes: + result -= exclude_nodes + + # Filter to explicit input nodes if provided + if nodes is not None: + # The amount of input nodes to filter to can be large and querying + # many nodes can be slow in Maya. As such we want to try and reduce + # it as much as possible, so we include the type filter to try and + # reduce the result of `maya.cmds.ls` here. + nodes = set(cmds.ls(nodes, long=True, type=types + ["dagNode"])) + if nodes: + result &= nodes + else: + return set() + + return result def get_id(node): diff --git a/client/ayon_core/hosts/maya/api/pipeline.py b/client/ayon_core/hosts/maya/api/pipeline.py index 2be452a22a..464e87814c 100644 --- a/client/ayon_core/hosts/maya/api/pipeline.py +++ b/client/ayon_core/hosts/maya/api/pipeline.py @@ -580,7 +580,8 @@ def on_save(): _remove_workfile_lock() # Generate ids of the current context on nodes in the scene - nodes = lib.get_id_required_nodes(referenced_nodes=False) + nodes = lib.get_id_required_nodes(referenced_nodes=False, + existing_ids=False) for node, new_id in lib.generate_ids(nodes): lib.set_id(node, new_id, overwrite=False) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids.py b/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids.py index ba748a4fc4..2d6f231cb5 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids.py @@ -60,7 +60,8 @@ class ValidateNodeIDs(pyblish.api.InstancePlugin): # We do want to check the referenced nodes as it might be # part of the end product. id_nodes = lib.get_id_required_nodes(referenced_nodes=True, - nodes=instance[:]) - invalid = [n for n in id_nodes if not lib.get_id(n)] - - return invalid + nodes=instance[:], + # Exclude those with already + # existing ids + existing_ids=False) + return id_nodes diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_in_database.py b/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_in_database.py index 5ca9690fd7..58978fb538 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_in_database.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_in_database.py @@ -43,11 +43,15 @@ class ValidateNodeIdsInDatabase(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance): - invalid = [] + nodes = instance[:] + if not nodes: + return # Get all id required nodes - id_required_nodes = lib.get_id_required_nodes(referenced_nodes=True, - nodes=instance[:]) + id_required_nodes = lib.get_id_required_nodes(referenced_nodes=False, + nodes=nodes) + if not id_required_nodes: + return # check ids against database ids project_name = instance.context.data["projectName"] @@ -58,6 +62,7 @@ class ValidateNodeIdsInDatabase(pyblish.api.InstancePlugin): } # Get all asset IDs + invalid = [] for node in id_required_nodes: cb_id = lib.get_id(node) From e73a4ef4bf11f65a20b1303ec0c2bb41535e102a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 29 Mar 2024 20:16:35 +0100 Subject: [PATCH 20/56] Only collect file dependencies if enabled in settings and only collect if renderlayer family is present --- .../publish/collect_file_dependencies.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/collect_file_dependencies.py b/client/ayon_core/hosts/maya/plugins/publish/collect_file_dependencies.py index 94fcc834e1..93b46c511b 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/collect_file_dependencies.py +++ b/client/ayon_core/hosts/maya/plugins/publish/collect_file_dependencies.py @@ -1,5 +1,3 @@ -import json - from maya import cmds import pyblish.api @@ -11,18 +9,24 @@ class CollectFileDependencies(pyblish.api.ContextPlugin): label = "Collect File Dependencies" order = pyblish.api.CollectorOrder - 0.49 hosts = ["maya"] + families = ["renderlayer"] + + @classmethod + def apply_settings(cls, project_settings, system_settings): + # Disable plug-in if not used for deadline submission anyway + settings = project_settings["deadline"]["publish"]["MayaSubmitDeadline"] # noqa + cls.enabled = settings.get("asset_dependencies", True) def process(self, context): - dependencies = [] + dependencies = set() for node in cmds.ls(type="file"): path = cmds.getAttr("{}.{}".format(node, "fileTextureName")) if path not in dependencies: - dependencies.append(path) + dependencies.add(path) for node in cmds.ls(type="AlembicNode"): path = cmds.getAttr("{}.{}".format(node, "abc_File")) if path not in dependencies: - dependencies.append(path) + dependencies.add(path) - context.data["fileDependencies"] = dependencies - self.log.debug(json.dumps(dependencies, indent=4)) + context.data["fileDependencies"] = list(dependencies) From 2bf1ca2a2df2fc5009cdd4d4d704e4a4c791b8d9 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 2 Apr 2024 10:03:59 +0200 Subject: [PATCH 21/56] Validate only when redshift renderer is used --- ...validate_current_renderlayer_renderable.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_current_renderlayer_renderable.py b/client/ayon_core/hosts/maya/plugins/publish/validate_current_renderlayer_renderable.py index da8468c1b6..045e22545c 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_current_renderlayer_renderable.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_current_renderlayer_renderable.py @@ -36,6 +36,12 @@ class ValidateCurrentRenderLayerIsRenderable(pyblish.api.ContextPlugin, if not context_plugin_should_run(self, context): return + # This validator only makes sense when publishing renderlayer instances + # with Redshift. We skip validation if there isn't any. + if not any(self.is_active_redshift_render_instance(instance) + for instance in context): + return + cameras = cmds.ls(type="camera", long=True) renderable = any(c for c in cameras if cmds.getAttr(c + ".renderable")) if not renderable: @@ -47,3 +53,22 @@ class ValidateCurrentRenderLayerIsRenderable(pyblish.api.ContextPlugin, ), description=inspect.getdoc(self) ) + + @staticmethod + def is_active_redshift_render_instance(instance) -> bool: + """Return whether instance is an active renderlayer instance set to + render with Redshift renderer.""" + if not instance.data.get("active", True): + return False + + # Check this before families just because it's a faster check + if not instance.data.get("renderer") == "redshift": + return False + + families = set() + families.add(instance.data.get("family")) + families.update(instance.data.get("families", [])) + if "renderlayer" not in families: + return False + + return True From 98bed9f5c610deb2f947d643573ce18bd85054da Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 2 Apr 2024 18:10:03 +0200 Subject: [PATCH 22/56] Update to latest avalon core --- client/ayon_core/hosts/fusion/api/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/fusion/api/plugin.py b/client/ayon_core/hosts/fusion/api/plugin.py index c2ecea93fc..80167189a5 100644 --- a/client/ayon_core/hosts/fusion/api/plugin.py +++ b/client/ayon_core/hosts/fusion/api/plugin.py @@ -126,7 +126,7 @@ class GenericCreateSaver(Creator): if ( original_product_name != product_name or tool.GetData("openpype.task") != data["task"] - or tool.GetData("openpype.asset") != data["asset"] + or tool.GetData("openpype.folderPath") != data["folderPath"] or original_format != data["creator_attributes"]["image_format"] ): self._configure_saver_tool(data, tool, product_name) From 6109803c1d48a0a302e158e81724aff96821f070 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 3 Apr 2024 16:08:09 +0200 Subject: [PATCH 23/56] Update instances using `CreateContext` to correctly update product names if needed - Also rely on task attributes for resolution and frame ranges instead of folder attributes --- client/ayon_core/hosts/maya/api/lib.py | 71 +++++++++++++++----------- 1 file changed, 42 insertions(+), 29 deletions(-) diff --git a/client/ayon_core/hosts/maya/api/lib.py b/client/ayon_core/hosts/maya/api/lib.py index 8d418813cd..52ac67f3e2 100644 --- a/client/ayon_core/hosts/maya/api/lib.py +++ b/client/ayon_core/hosts/maya/api/lib.py @@ -37,7 +37,7 @@ from ayon_core.pipeline import ( AYON_CONTAINER_ID, ) from ayon_core.lib import NumberDef -from ayon_core.pipeline.context_tools import get_current_folder_entity +from ayon_core.pipeline.context_tools import get_current_task_entity from ayon_core.pipeline.create import CreateContext from ayon_core.lib.profiles_filtering import filter_profiles @@ -2636,7 +2636,7 @@ def reset_scene_resolution(): None """ - folder_attributes = get_current_folder_entity()["attrib"] + folder_attributes = get_current_task_entity(fields={"attrib"})["attrib"] # Set resolution width = folder_attributes.get("resolutionWidth", 1920) @@ -3244,33 +3244,46 @@ def update_content_on_context_change(): """ This will update scene content to match new folder on context change """ - scene_sets = cmds.listSets(allSets=True) - folder_entity = get_current_folder_entity() - folder_attributes = folder_entity["attrib"] - new_folder_path = folder_entity["path"] - for s in scene_sets: - try: - if cmds.getAttr("{}.id".format(s)) in { - AYON_INSTANCE_ID, AVALON_INSTANCE_ID - }: - attr = cmds.listAttr(s) - print(s) - if "folderPath" in attr: - print( - " - setting folder to: [ {} ]".format(new_folder_path) - ) - cmds.setAttr( - "{}.folderPath".format(s), - new_folder_path, type="string" - ) - if "frameStart" in attr: - cmds.setAttr("{}.frameStart".format(s), - folder_attributes["frameStart"]) - if "frameEnd" in attr: - cmds.setAttr("{}.frameEnd".format(s), - folder_attributes["frameEnd"],) - except ValueError: - pass + + host = registered_host() + create_context = CreateContext(host) + folder_entity = get_current_task_entity(fields={"attrib"}) + + instance_values = { + "folderPath": create_context.get_current_folder_path(), + "task": create_context.get_current_task_name(), + } + creator_attribute_values = { + "frameStart": folder_entity["attrib"]["frameStart"], + "frameEnd": folder_entity["attrib"]["frameEnd"], + } + + has_changes = False + for instance in create_context.instances: + for key, value in instance_values.items(): + if key not in instance or instance[key] == value: + continue + + # Update instance value + print(f"Updating {instance.product_name} {key} to: {value}") + instance[key] = value + has_changes = True + + creator_attributes = instance.creator_attributes + for key, value in creator_attribute_values.items(): + if ( + key not in creator_attributes + or creator_attributes[key] == value + ): + continue + + # Update instance creator attribute value + print(f"Updating {instance.product_name} {key} to: {value}") + instance[key] = value + has_changes = True + + if has_changes: + create_context.save_changes() def show_message(title, msg): From 5acf6f3b08fc3423c4af9a7bea4fc442a50b80a8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 3 Apr 2024 16:12:51 +0200 Subject: [PATCH 24/56] Update variable name --- client/ayon_core/hosts/maya/api/lib.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/hosts/maya/api/lib.py b/client/ayon_core/hosts/maya/api/lib.py index 52ac67f3e2..8bf753693f 100644 --- a/client/ayon_core/hosts/maya/api/lib.py +++ b/client/ayon_core/hosts/maya/api/lib.py @@ -2636,12 +2636,12 @@ def reset_scene_resolution(): None """ - folder_attributes = get_current_task_entity(fields={"attrib"})["attrib"] + task_attributes = get_current_task_entity(fields={"attrib"})["attrib"] # Set resolution - width = folder_attributes.get("resolutionWidth", 1920) - height = folder_attributes.get("resolutionHeight", 1080) - pixelAspect = folder_attributes.get("pixelAspect", 1) + width = task_attributes.get("resolutionWidth", 1920) + height = task_attributes.get("resolutionHeight", 1080) + pixelAspect = task_attributes.get("pixelAspect", 1) set_scene_resolution(width, height, pixelAspect) From ef0cc38d7f7b3cb959444673c9f28d36ce861338 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 3 Apr 2024 16:13:02 +0200 Subject: [PATCH 25/56] Cosmetics --- client/ayon_core/hosts/maya/api/lib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/hosts/maya/api/lib.py b/client/ayon_core/hosts/maya/api/lib.py index 8bf753693f..07ae65748e 100644 --- a/client/ayon_core/hosts/maya/api/lib.py +++ b/client/ayon_core/hosts/maya/api/lib.py @@ -2641,9 +2641,9 @@ def reset_scene_resolution(): # Set resolution width = task_attributes.get("resolutionWidth", 1920) height = task_attributes.get("resolutionHeight", 1080) - pixelAspect = task_attributes.get("pixelAspect", 1) + pixel_aspect = task_attributes.get("pixelAspect", 1) - set_scene_resolution(width, height, pixelAspect) + set_scene_resolution(width, height, pixel_aspect) def set_context_settings( From c48364cd0546e50d8497a3fc6e163f13d25931a7 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 3 Apr 2024 16:13:32 +0200 Subject: [PATCH 26/56] Update docstring --- client/ayon_core/hosts/maya/api/lib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/hosts/maya/api/lib.py b/client/ayon_core/hosts/maya/api/lib.py index 07ae65748e..72f680943f 100644 --- a/client/ayon_core/hosts/maya/api/lib.py +++ b/client/ayon_core/hosts/maya/api/lib.py @@ -2629,8 +2629,8 @@ def reset_frame_range(playback=True, render=True, fps=True): def reset_scene_resolution(): """Apply the scene resolution from the project definition - scene resolution can be overwritten by an folder if the folder.attrib - contains any information regarding scene resolution . + The scene resolution will be retrieved from the current task entity's + attributes. Returns: None From 2803b8521f701291547325053df8de9ff987f8e0 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 3 Apr 2024 16:29:52 +0200 Subject: [PATCH 27/56] Remove enforced updating of context settings (this is replaced by the "update on context change" dialog) --- client/ayon_core/hosts/maya/api/pipeline.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/client/ayon_core/hosts/maya/api/pipeline.py b/client/ayon_core/hosts/maya/api/pipeline.py index 8e6e2ccd8a..eb46088ecd 100644 --- a/client/ayon_core/hosts/maya/api/pipeline.py +++ b/client/ayon_core/hosts/maya/api/pipeline.py @@ -653,10 +653,6 @@ def on_task_changed(): "Can't set project for new context because path does not exist: {}" ).format(workdir)) - with lib.suspended_refresh(): - lib.set_context_settings() - lib.update_content_on_context_change() - global _about_to_save if not lib.IS_HEADLESS and _about_to_save: # Let's prompt the user to update the context settings or not From 42e7e32264e034fc35af8b4c75e4b1bd8c12634d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 3 Apr 2024 19:47:38 +0200 Subject: [PATCH 28/56] Cache the result of the folder ids query so we don't query per instance --- .../publish/validate_node_ids_in_database.py | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_in_database.py b/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_in_database.py index 5ca9690fd7..85850d413b 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_in_database.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_in_database.py @@ -50,12 +50,7 @@ class ValidateNodeIdsInDatabase(pyblish.api.InstancePlugin): nodes=instance[:]) # check ids against database ids - project_name = instance.context.data["projectName"] - folder_entities = ayon_api.get_folders(project_name, fields={"id"}) - folder_ids = { - folder_entity["id"] - for folder_entity in folder_entities - } + folder_ids = cls.get_project_folder_ids(context=instance.context) # Get all asset IDs for node in id_required_nodes: @@ -71,3 +66,22 @@ class ValidateNodeIdsInDatabase(pyblish.api.InstancePlugin): invalid.append(node) return invalid + + @classmethod + def get_project_folder_ids(cls, context): + # We query the database only for the first instance instead of + # per instance by storing a cache in the context + key = "__cache_project_folders_ids" + if key in context.data: + return context.data[key] + + # check ids against database + project_name = context.data["projectName"] + folder_entities = ayon_api.get_folders(project_name, fields={"id"}) + folder_ids = { + folder_entity["id"] + for folder_entity in folder_entities + } + + context.data[key] = folder_ids + return folder_ids From 8daf9d1d39b4779f82b97c2f5e7b12791642f820 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 3 Apr 2024 19:50:02 +0200 Subject: [PATCH 29/56] Opt-out early if there's nothing to validate anyway --- .../maya/plugins/publish/validate_node_ids_in_database.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_in_database.py b/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_in_database.py index 85850d413b..a26a4b6843 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_in_database.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_in_database.py @@ -43,16 +43,17 @@ class ValidateNodeIdsInDatabase(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance): - invalid = [] - # Get all id required nodes id_required_nodes = lib.get_id_required_nodes(referenced_nodes=True, nodes=instance[:]) + if not id_required_nodes: + return [] # check ids against database ids folder_ids = cls.get_project_folder_ids(context=instance.context) # Get all asset IDs + invalid = [] for node in id_required_nodes: cb_id = lib.get_id(node) From ed99521224f1278974d556b9fbb7a253848f2215 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 3 Apr 2024 19:50:09 +0200 Subject: [PATCH 30/56] Add docstring --- .../plugins/publish/validate_node_ids_in_database.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_in_database.py b/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_in_database.py index a26a4b6843..814202c66c 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_in_database.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_in_database.py @@ -70,6 +70,15 @@ class ValidateNodeIdsInDatabase(pyblish.api.InstancePlugin): @classmethod def get_project_folder_ids(cls, context): + """Return all folder ids in the current project. + + Arguments: + context (pyblish.api.Context): The publish context. + + Returns: + set[str]: All folder ids in the current project. + + """ # We query the database only for the first instance instead of # per instance by storing a cache in the context key = "__cache_project_folders_ids" From 322615906d7115ca1962f97f9deb9ac96c381ed1 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 3 Apr 2024 19:50:31 +0200 Subject: [PATCH 31/56] Cosmetics --- .../maya/plugins/publish/validate_node_ids_in_database.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_in_database.py b/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_in_database.py index 814202c66c..94f3aad8aa 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_in_database.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_in_database.py @@ -37,8 +37,8 @@ class ValidateNodeIdsInDatabase(pyblish.api.InstancePlugin): invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( - ("Found folder ids which are not related to " - "current project in instance: `{}`").format(instance.name)) + "Found folder ids which are not related to " + "current project in instance: `{}`".format(instance.name)) @classmethod def get_invalid(cls, instance): From a5c19963d5f54ac0007b60e531c67eb534cc7d5a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 3 Apr 2024 19:56:12 +0200 Subject: [PATCH 32/56] Improve key --- .../hosts/maya/plugins/publish/validate_node_ids_in_database.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_in_database.py b/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_in_database.py index 94f3aad8aa..8e29edc0ce 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_in_database.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_in_database.py @@ -81,7 +81,7 @@ class ValidateNodeIdsInDatabase(pyblish.api.InstancePlugin): """ # We query the database only for the first instance instead of # per instance by storing a cache in the context - key = "__cache_project_folders_ids" + key = "__cache_project_folder_ids" if key in context.data: return context.data[key] From eb0e1c9cf7b1f40a1642a63344dd8354632c21ea Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 3 Apr 2024 19:59:23 +0200 Subject: [PATCH 33/56] Opt-out even earlier if instance has no members/nodes. --- .../maya/plugins/publish/validate_node_ids_in_database.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_in_database.py b/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_in_database.py index 8e29edc0ce..bb6b590f5c 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_in_database.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_in_database.py @@ -43,9 +43,13 @@ class ValidateNodeIdsInDatabase(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance): + nodes = instance[:] + if not nodes: + return + # Get all id required nodes id_required_nodes = lib.get_id_required_nodes(referenced_nodes=True, - nodes=instance[:]) + nodes=nodes) if not id_required_nodes: return [] From daa07a8a7495ac7c1c7961a6100b9fad35d6909d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 3 Apr 2024 20:09:59 +0200 Subject: [PATCH 34/56] Improve report --- .../maya/plugins/publish/validate_node_ids_unique.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_unique.py b/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_unique.py index f4994922ce..735441a921 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_unique.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_unique.py @@ -41,7 +41,7 @@ class ValidateNodeIdsUnique(pyblish.api.InstancePlugin): if invalid: label = "Nodes found with non-unique folder ids" raise PublishValidationError( - message="{}: {}".format(label, invalid), + message="{}, see log".format(label), title="Non-unique folder ids on nodes", description="{}\n- {}".format(label, "\n- ".join(sorted(invalid))) @@ -70,7 +70,12 @@ class ValidateNodeIdsUnique(pyblish.api.InstancePlugin): _iteritems = getattr(ids, "iteritems", ids.items) for _ids, members in _iteritems(): if len(members) > 1: - cls.log.error("ID found on multiple nodes: '%s'" % members) + members_text = "\n".join( + "- {}".format(member) for member in sorted(members) + ) + cls.log.error( + "ID found on multiple nodes:\n{}".format(members_text) + ) invalid.extend(members) return invalid From be9c8a531b47e1775220cafd7d7cef06c626ba33 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 3 Apr 2024 20:10:19 +0200 Subject: [PATCH 35/56] Move import to top --- .../hosts/maya/plugins/publish/validate_node_ids_unique.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_unique.py b/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_unique.py index 735441a921..6c838d2b86 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_unique.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_unique.py @@ -8,6 +8,8 @@ from ayon_core.pipeline.publish import ( import ayon_core.hosts.maya.api.action from ayon_core.hosts.maya.api import lib +from maya import cmds + class ValidateNodeIdsUnique(pyblish.api.InstancePlugin): """Validate the nodes in the instance have a unique Colorbleed Id @@ -54,7 +56,6 @@ class ValidateNodeIdsUnique(pyblish.api.InstancePlugin): # Check only non intermediate shapes # todo: must the instance itself ensure to have no intermediates? # todo: how come there are intermediates? - from maya import cmds instance_members = cmds.ls(instance, noIntermediate=True, long=True) # Collect each id with their members From b4139cb10ea8523e107993f409dbfa1cb0086ed6 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 3 Apr 2024 20:11:19 +0200 Subject: [PATCH 36/56] Remove Python 2 optimization --- .../hosts/maya/plugins/publish/validate_node_ids_unique.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_unique.py b/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_unique.py index 6c838d2b86..6b44a307d2 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_unique.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_unique.py @@ -68,8 +68,7 @@ class ValidateNodeIdsUnique(pyblish.api.InstancePlugin): # Take only the ids with more than one member invalid = list() - _iteritems = getattr(ids, "iteritems", ids.items) - for _ids, members in _iteritems(): + for members in ids.values(): if len(members) > 1: members_text = "\n".join( "- {}".format(member) for member in sorted(members) From ca04b809c3e12614497739dd39818b4a4a4b8a00 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 4 Apr 2024 01:43:19 +0200 Subject: [PATCH 37/56] Compute workdir based on instance folder path and task data if it does not match current context --- client/ayon_core/hosts/fusion/api/plugin.py | 31 ++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/fusion/api/plugin.py b/client/ayon_core/hosts/fusion/api/plugin.py index 80167189a5..efe8269120 100644 --- a/client/ayon_core/hosts/fusion/api/plugin.py +++ b/client/ayon_core/hosts/fusion/api/plugin.py @@ -16,6 +16,12 @@ from ayon_core.pipeline import ( AVALON_INSTANCE_ID, AYON_INSTANCE_ID, ) +from ayon_core.pipeline.workfile import get_workdir +from ayon_api import ( + get_project, + get_folder_by_path, + get_task_by_name +) class GenericCreateSaver(Creator): @@ -147,7 +153,30 @@ class GenericCreateSaver(Creator): folder_path = formatting_data["folderPath"] folder_name = folder_path.rsplit("/", 1)[-1] - workdir = os.path.normpath(os.getenv("AYON_WORKDIR")) + # If the folder path and task do not match the current context then the + # workdir is not just the `AYON_WORKDIR`. Hence, we need to actually + # compute the resulting workdir + if ( + data["folderPath"] == self.create_context.get_current_folder_path() + and data["task"] == self.create_context.get_current_task_name() + ): + workdir = os.path.normpath(os.getenv("AYON_WORKDIR")) + else: + # TODO: Optimize this logic + project_name = self.create_context.get_current_project_name() + project_entity = get_project(project_name) + folder_entity = get_folder_by_path(project_name, + data["folderPath"]) + task_entity = get_task_by_name(project_name, + folder_id=folder_entity["id"], + task_name=data["task"]) + workdir = get_workdir( + project_entity=project_entity, + folder_entity=folder_entity, + task_entity=task_entity, + host_name=self.create_context.host_name, + ) + formatting_data.update({ "workdir": workdir, "frame": "0" * frame_padding, From 8e107744eb31d770e34e192b2f78d7090d5bc86b Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 4 Apr 2024 10:30:43 +0100 Subject: [PATCH 38/56] Account for no nodes in container --- client/ayon_core/hosts/maya/api/workfile_template_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/maya/api/workfile_template_builder.py b/client/ayon_core/hosts/maya/api/workfile_template_builder.py index cb25a722f0..75386d7e64 100644 --- a/client/ayon_core/hosts/maya/api/workfile_template_builder.py +++ b/client/ayon_core/hosts/maya/api/workfile_template_builder.py @@ -286,7 +286,7 @@ class MayaPlaceholderLoadPlugin(PlaceholderPlugin, PlaceholderLoadMixin): if not container: return - roots = cmds.sets(container, q=True) + roots = cmds.sets(container, q=True) or [] ref_node = None try: ref_node = get_reference_node(roots) From 368444a7f25c2d9a6d044f605a860677f268d2d8 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 4 Apr 2024 22:21:11 +0800 Subject: [PATCH 39/56] use cmds.setattr instead of self._get_attr() --- client/ayon_core/hosts/maya/api/lib_renderproducts.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/hosts/maya/api/lib_renderproducts.py b/client/ayon_core/hosts/maya/api/lib_renderproducts.py index 5f3917f642..6b73639040 100644 --- a/client/ayon_core/hosts/maya/api/lib_renderproducts.py +++ b/client/ayon_core/hosts/maya/api/lib_renderproducts.py @@ -1133,9 +1133,11 @@ class RenderProductsRedshift(ARenderProducts): aovs = list(set(aovs) - set(ref_aovs)) products = [] + # global_aov_enabled = bool( + # self._get_attr("redshiftOptions.aovGlobalEnableMode") + # ) global_aov_enabled = bool( - self._get_attr("redshiftOptions.aovGlobalEnableMode") - ) + cmds.getAttr("redshiftOptions.aovGlobalEnableMode")) colorspace = lib.get_color_management_output_transform() if not global_aov_enabled: # only beauty output From 4a04bd1397a9a5e564c6b5d7e7f7262b53588ccc Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 4 Apr 2024 16:35:49 +0200 Subject: [PATCH 40/56] fix update of context change --- client/ayon_core/tools/workfiles/control.py | 38 ++++++++++----------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/client/ayon_core/tools/workfiles/control.py b/client/ayon_core/tools/workfiles/control.py index 7fa7af1662..2cdcd37d53 100644 --- a/client/ayon_core/tools/workfiles/control.py +++ b/client/ayon_core/tools/workfiles/control.py @@ -659,16 +659,7 @@ class BaseWorkfileController( folder_id != self.get_current_folder_id() or task_name != self.get_current_task_name() ): - folder_entity = ayon_api.get_folder_by_id( - event_data["project_name"], - event_data["folder_id"], - ) - task_entity = ayon_api.get_task_by_name( - event_data["project_name"], - event_data["folder_id"], - event_data["task_name"] - ) - change_current_context(folder_entity, task_entity) + self._change_current_context(project_name, folder_id, task_id) self._host_open_workfile(filepath) @@ -710,16 +701,8 @@ class BaseWorkfileController( folder_id != self.get_current_folder_id() or task_name != self.get_current_task_name() ): - folder_entity = ayon_api.get_folder_by_id( - project_name, folder["id"] - ) - task_entity = ayon_api.get_task_by_name( - project_name, folder["id"], task_name - ) - change_current_context( - folder_entity, - task_entity, - template_key=template_key + self._change_current_context( + project_name, folder_id, task_id, template_key ) # Save workfile @@ -745,3 +728,18 @@ class BaseWorkfileController( # Trigger after save events emit_event("workfile.save.after", event_data, source="workfiles.tool") self.reset() + + def _change_current_context( + self, project_name, folder_id, task_id, template_key=None + ): + # Change current context + folder_entity = self.get_folder_entity(project_name, folder_id) + task_entity = self.get_task_entity(project_name, task_id) + change_current_context( + folder_entity, + task_entity, + template_key=template_key + ) + self._current_folder_id = folder_entity["id"] + self._current_folder_path = folder_entity["path"] + self._current_task_name = task_entity["name"] From 1e0c115a5ad52375b63060250e32fd8a95037f82 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 4 Apr 2024 16:38:00 +0200 Subject: [PATCH 41/56] Improve artist-facing report --- .../plugins/publish/validate_model_content.py | 39 +++++++++++++------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_model_content.py b/client/ayon_core/hosts/maya/plugins/publish/validate_model_content.py index 97d602a9ad..015f4c11ed 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_model_content.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_model_content.py @@ -1,3 +1,5 @@ +import inspect + from maya import cmds import pyblish.api @@ -14,8 +16,7 @@ class ValidateModelContent(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Adheres to the content of 'model' product type - - Must have one top group. (configurable) - - Must only contain: transforms, meshes and groups + See `get_description` for more details. """ @@ -35,7 +36,8 @@ class ValidateModelContent(pyblish.api.InstancePlugin, content_instance = instance.data.get("setMembers", None) if not content_instance: - cls.log.error("Instance has no nodes!") + cls.log.error("Model instance has no nodes. " + "It is not allowed to be empty") return [instance.data["instance_node"]] # All children will be included in the extracted export so we also @@ -53,10 +55,13 @@ class ValidateModelContent(pyblish.api.InstancePlugin, invalid = set(nodes) - set(valid) if invalid: + # List as bullet points + invalid_bullets = "\n".join(f"- {node}" for node in invalid) + cls.log.error( - "These nodes are not allowed: {}.\n" - "The valid node types are: {}".format(", ".join(invalid), - ", ".join(cls.allowed)) + "These nodes are not allowed:\n{}\n" + "The valid node types are: {}".format( + invalid_bullets, ", ".join(cls.allowed)) ) return list(invalid) @@ -113,10 +118,20 @@ class ValidateModelContent(pyblish.api.InstancePlugin, raise PublishValidationError( title="Model content is invalid", message="Model content is invalid. See log for more details.", - description=( - "## Model content is invalid\n" - "Your model instance does not adhere to the rules of a " - "model.\n\n" - "See log for more details." - ) + description=self.get_description() ) + + @classmethod + def get_description(cls): + return inspect.cleandoc(f""" + ### Model content is invalid + + Your model instance does not adhere to the rules of a + model product type: + + - Must have at least one visible shape in it, like a mesh. + - Must have one root node. When exporting multiple meshes they + must be inside a group. + - May only contain the following node types: + {", ".join(cls.allowed)} + """) From 2701d4bf8aeddb4da0841b49d7d06451ee5f06d8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 4 Apr 2024 16:46:15 +0200 Subject: [PATCH 42/56] removed unnecessary `reset` call --- client/ayon_core/tools/workfiles/control.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/tools/workfiles/control.py b/client/ayon_core/tools/workfiles/control.py index 2cdcd37d53..3048e6be94 100644 --- a/client/ayon_core/tools/workfiles/control.py +++ b/client/ayon_core/tools/workfiles/control.py @@ -727,7 +727,6 @@ class BaseWorkfileController( # Trigger after save events emit_event("workfile.save.after", event_data, source="workfiles.tool") - self.reset() def _change_current_context( self, project_name, folder_id, task_id, template_key=None From fca484dd01cb84f1b14f23d1d50690955eeae57a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 4 Apr 2024 17:11:26 +0200 Subject: [PATCH 43/56] Improve log readability --- .../hosts/maya/plugins/publish/validate_model_content.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_model_content.py b/client/ayon_core/hosts/maya/plugins/publish/validate_model_content.py index 015f4c11ed..1001bee91f 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_model_content.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_model_content.py @@ -59,7 +59,7 @@ class ValidateModelContent(pyblish.api.InstancePlugin, invalid_bullets = "\n".join(f"- {node}" for node in invalid) cls.log.error( - "These nodes are not allowed:\n{}\n" + "These nodes are not allowed:\n{}\n\n" "The valid node types are: {}".format( invalid_bullets, ", ".join(cls.allowed)) ) From dc21231c291ebffb532151806e2e696ac3510fdc Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 4 Apr 2024 23:16:12 +0800 Subject: [PATCH 44/56] exposes asString as arguments for getting attributes for renderlayer & renderlayersetup --- client/ayon_core/hosts/maya/api/lib.py | 9 +++++---- client/ayon_core/hosts/maya/api/lib_renderproducts.py | 10 ++++------ client/ayon_core/hosts/maya/api/lib_rendersetup.py | 6 +++--- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/client/ayon_core/hosts/maya/api/lib.py b/client/ayon_core/hosts/maya/api/lib.py index 060c16056c..89582bac8b 100644 --- a/client/ayon_core/hosts/maya/api/lib.py +++ b/client/ayon_core/hosts/maya/api/lib.py @@ -3129,7 +3129,7 @@ def load_capture_preset(data): return options -def get_attr_in_layer(attr, layer): +def get_attr_in_layer(attr, layer, as_string=True): """Return attribute value in specified renderlayer. Same as cmds.getAttr but this gets the attribute's value in a @@ -3156,7 +3156,8 @@ def get_attr_in_layer(attr, layer): try: if cmds.mayaHasRenderSetup(): from . import lib_rendersetup - return lib_rendersetup.get_attr_in_layer(attr, layer) + return lib_rendersetup.get_attr_in_layer( + attr, layer, as_string=as_string) except AttributeError: pass @@ -3173,7 +3174,7 @@ def get_attr_in_layer(attr, layer): type="renderLayer") or [] connections = filter(lambda x: x.endswith(".plug"), connections) if not connections: - return cmds.getAttr(attr) + return cmds.getAttr(attr, asString=as_string) # Some value types perform a conversion when assigning # TODO: See if there's a maya method to allow this conversion @@ -3215,7 +3216,7 @@ def get_attr_in_layer(attr, layer): value *= conversion return value - return cmds.getAttr(attr) + return cmds.getAttr(attr, asString=as_string) def fix_incompatible_containers(): diff --git a/client/ayon_core/hosts/maya/api/lib_renderproducts.py b/client/ayon_core/hosts/maya/api/lib_renderproducts.py index 6b73639040..832d1c21c2 100644 --- a/client/ayon_core/hosts/maya/api/lib_renderproducts.py +++ b/client/ayon_core/hosts/maya/api/lib_renderproducts.py @@ -297,7 +297,7 @@ class ARenderProducts: """ return self._get_attr("defaultRenderGlobals", attribute) - def _get_attr(self, node_attr, attribute=None): + def _get_attr(self, node_attr, attribute=None, as_string=True): """Return the value of the attribute in the renderlayer For readability this allows passing in the attribute in two ways. @@ -317,7 +317,7 @@ class ARenderProducts: else: plug = "{}.{}".format(node_attr, attribute) - return lib.get_attr_in_layer(plug, layer=self.layer) + return lib.get_attr_in_layer(plug, layer=self.layer, as_string=as_string) @staticmethod def extract_separator(file_prefix): @@ -1133,11 +1133,9 @@ class RenderProductsRedshift(ARenderProducts): aovs = list(set(aovs) - set(ref_aovs)) products = [] - # global_aov_enabled = bool( - # self._get_attr("redshiftOptions.aovGlobalEnableMode") - # ) global_aov_enabled = bool( - cmds.getAttr("redshiftOptions.aovGlobalEnableMode")) + self._get_attr("redshiftOptions.aovGlobalEnableMode", as_string=False) + ) colorspace = lib.get_color_management_output_transform() if not global_aov_enabled: # only beauty output diff --git a/client/ayon_core/hosts/maya/api/lib_rendersetup.py b/client/ayon_core/hosts/maya/api/lib_rendersetup.py index c2b5ec843c..6dca8eb6dd 100644 --- a/client/ayon_core/hosts/maya/api/lib_rendersetup.py +++ b/client/ayon_core/hosts/maya/api/lib_rendersetup.py @@ -77,7 +77,7 @@ def get_rendersetup_layer(layer): if conn.endswith(".legacyRenderLayer")), None) -def get_attr_in_layer(node_attr, layer): +def get_attr_in_layer(node_attr, layer, as_string=True): """Return attribute value in Render Setup layer. This will only work for attributes which can be @@ -124,7 +124,7 @@ def get_attr_in_layer(node_attr, layer): node = history_overrides[-1] if history_overrides else override node_attr_ = node + ".original" - return get_attribute(node_attr_, asString=True) + return get_attribute(node_attr_, asString=as_string) layer = get_rendersetup_layer(layer) rs = renderSetup.instance() @@ -144,7 +144,7 @@ def get_attr_in_layer(node_attr, layer): # we will let it error out. rs.switchToLayer(current_layer) - return get_attribute(node_attr, asString=True) + return get_attribute(node_attr, asString=as_string) overrides = get_attr_overrides(node_attr, layer) default_layer_value = get_default_layer_value(node_attr) From 8573a0f6cf63103284466d84c1c48ea6067e9fb0 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 4 Apr 2024 17:51:00 +0200 Subject: [PATCH 45/56] Improve Creator error for new users --- .../hosts/maya/plugins/create/create_render.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/hosts/maya/plugins/create/create_render.py b/client/ayon_core/hosts/maya/plugins/create/create_render.py index 213d5b543e..e5a8d4dbd8 100644 --- a/client/ayon_core/hosts/maya/plugins/create/create_render.py +++ b/client/ayon_core/hosts/maya/plugins/create/create_render.py @@ -40,8 +40,15 @@ class CreateRenderlayer(plugin.RenderlayerCreator): def create(self, product_name, instance_data, pre_create_data): # Only allow a single render instance to exist if self._get_singleton_node(): - raise CreatorError("A Render instance already exists - only " - "one can be configured.") + raise CreatorError( + "A Render instance already exists - only one can be " + "configured.\n\n" + "To render multiple render layers, create extra Render Setup " + "Layers via Maya's Render Setup UI.\n" + "Then refresh the publisher to detect the new layers for " + "rendering.\n\n" + "With a render instance present all Render Setup layers in " + "your workfile are renderable instances.") # Apply default project render settings on create if self.render_settings.get("apply_render_settings"): From 55070d9528fd693ee6353d113d7b8e4ed3bc3838 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A0=20Serra=20Arrizabalaga?= Date: Thu, 4 Apr 2024 23:54:57 +0200 Subject: [PATCH 46/56] Fix typos --- .../ayon_core/hosts/hiero/plugins/create/create_shot_clip.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/hosts/hiero/plugins/create/create_shot_clip.py b/client/ayon_core/hosts/hiero/plugins/create/create_shot_clip.py index 2985a81317..1fc808fdd1 100644 --- a/client/ayon_core/hosts/hiero/plugins/create/create_shot_clip.py +++ b/client/ayon_core/hosts/hiero/plugins/create/create_shot_clip.py @@ -137,7 +137,7 @@ class CreateShotClip(phiero.Creator): "value": ["", "main", "bg", "fg", "bg", "animatic"], "type": "QComboBox", - "label": "pRODUCT Name", + "label": "Product Name", "target": "ui", "toolTip": "chose product name pattern, if is selected, name of track layer will be used", # noqa "order": 0}, @@ -159,7 +159,7 @@ class CreateShotClip(phiero.Creator): "type": "QCheckBox", "label": "Include audio", "target": "tag", - "toolTip": "Process productS with corresponding audio", # noqa + "toolTip": "Process products with corresponding audio", # noqa "order": 3}, "sourceResolution": { "value": False, From 26b7a53656062625fed2c62f1573ad0166a6dbc3 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 5 Apr 2024 15:10:36 +0800 Subject: [PATCH 47/56] add docstring and make sure the backward compatible is converting to string value as before --- client/ayon_core/hosts/maya/api/lib.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/hosts/maya/api/lib.py b/client/ayon_core/hosts/maya/api/lib.py index 89582bac8b..91f60824b6 100644 --- a/client/ayon_core/hosts/maya/api/lib.py +++ b/client/ayon_core/hosts/maya/api/lib.py @@ -3147,6 +3147,7 @@ def get_attr_in_layer(attr, layer, as_string=True): Args: attr (str): attribute name, ex. "node.attribute" layer (str): layer name + as_string (bool): whether attribute should convert to a string value Returns: The return value from `maya.cmds.getAttr` @@ -3165,7 +3166,7 @@ def get_attr_in_layer(attr, layer, as_string=True): current_layer = cmds.editRenderLayerGlobals(query=True, currentRenderLayer=True) if layer == current_layer: - return cmds.getAttr(attr) + return cmds.getAttr(attr, asString=as_string) connections = cmds.listConnections(attr, plugs=True, @@ -3174,7 +3175,7 @@ def get_attr_in_layer(attr, layer, as_string=True): type="renderLayer") or [] connections = filter(lambda x: x.endswith(".plug"), connections) if not connections: - return cmds.getAttr(attr, asString=as_string) + return cmds.getAttr(attr) # Some value types perform a conversion when assigning # TODO: See if there's a maya method to allow this conversion From 51569b17dbe3dc1113b316e148a45cbd9d3ee841 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A0=20Serra=20Arrizabalaga?= Date: Fri, 5 Apr 2024 22:38:23 +0200 Subject: [PATCH 48/56] Pass env var used to login to ayon-shotgrid addon --- .../deadline/plugins/publish/submit_publish_cache_job.py | 2 +- .../modules/deadline/plugins/publish/submit_publish_job.py | 2 +- .../plugins/publish/create_publish_royalrender_job.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_publish_cache_job.py b/client/ayon_core/modules/deadline/plugins/publish/submit_publish_cache_job.py index 910b2e46db..4e4657d886 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_publish_cache_job.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_publish_cache_job.py @@ -67,7 +67,7 @@ class ProcessSubmittedCacheJobOnFarm(pyblish.api.InstancePlugin, "FTRACK_SERVER", "AYON_APP_NAME", "AYON_USERNAME", - "OPENPYPE_SG_USER", + "AYON_SG_USERNAME", "KITSU_LOGIN", "KITSU_PWD" ] diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_publish_job.py b/client/ayon_core/modules/deadline/plugins/publish/submit_publish_job.py index af5839d0cf..8def9cc63c 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_publish_job.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_publish_job.py @@ -130,7 +130,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, "FTRACK_SERVER", "AYON_APP_NAME", "AYON_USERNAME", - "OPENPYPE_SG_USER", + "AYON_SG_USERNAME", "KITSU_LOGIN", "KITSU_PWD" ] diff --git a/client/ayon_core/modules/royalrender/plugins/publish/create_publish_royalrender_job.py b/client/ayon_core/modules/royalrender/plugins/publish/create_publish_royalrender_job.py index 662913cadf..f3287b7638 100644 --- a/client/ayon_core/modules/royalrender/plugins/publish/create_publish_royalrender_job.py +++ b/client/ayon_core/modules/royalrender/plugins/publish/create_publish_royalrender_job.py @@ -65,7 +65,7 @@ class CreatePublishRoyalRenderJob(pyblish.api.InstancePlugin, "FTRACK_SERVER", "AYON_APP_NAME", "AYON_USERNAME", - "OPENPYPE_SG_USER", + "AYON_SG_USERNAME", ] priority = 50 From 78074e4d16324041d40091fe3412de6198e2959d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A0=20Serra=20Arrizabalaga?= Date: Fri, 5 Apr 2024 22:38:29 +0200 Subject: [PATCH 49/56] Fix typo --- .../hosts/hiero/plugins/publish/extract_thumbnail.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/hosts/hiero/plugins/publish/extract_thumbnail.py b/client/ayon_core/hosts/hiero/plugins/publish/extract_thumbnail.py index bcaf5308d9..3599a830d2 100644 --- a/client/ayon_core/hosts/hiero/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/hosts/hiero/plugins/publish/extract_thumbnail.py @@ -4,12 +4,12 @@ import pyblish.api from ayon_core.pipeline import publish -class ExtractThumnail(publish.Extractor): +class ExtractThumbnail(publish.Extractor): """ - Extractor for track item's tumnails + Extractor for track item's tumbnails """ - label = "Extract Thumnail" + label = "Extract Thumbnail" order = pyblish.api.ExtractorOrder families = ["plate", "take"] hosts = ["hiero"] @@ -48,7 +48,7 @@ class ExtractThumnail(publish.Extractor): self.log.debug( "__ thumb_path: `{}`, frame: `{}`".format(thumbnail, thumb_frame)) - self.log.info("Thumnail was generated to: {}".format(thumb_path)) + self.log.info("Thumbnail was generated to: {}".format(thumb_path)) thumb_representation = { 'files': thumb_file, 'stagingDir': staging_dir, From a08b1786b0891a3cd4e6bb5d8d557f3bf749b7c4 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 8 Apr 2024 08:47:50 +0100 Subject: [PATCH 50/56] Account for multiple slate frames. --- .../hosts/nuke/plugins/load/load_clip.py | 23 ++++++++----------- .../plugins/publish/extract_slate_frame.py | 2 +- .../plugins/publish/extract_slate_data.py | 2 +- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/client/ayon_core/hosts/nuke/plugins/load/load_clip.py b/client/ayon_core/hosts/nuke/plugins/load/load_clip.py index 86a6d359c3..062a5295ed 100644 --- a/client/ayon_core/hosts/nuke/plugins/load/load_clip.py +++ b/client/ayon_core/hosts/nuke/plugins/load/load_clip.py @@ -132,16 +132,15 @@ class LoadClip(plugin.NukeLoader): # If a slate is present, the frame range is 1 frame longer for movies, # but file sequences its the first frame that is 1 frame lower. - slate_frame = repre_entity["data"].get("slateFrame", False) - if slate_frame: - extension = "." + repre_entity["context"]["ext"] + slate_frames = repre_entity["data"].get("slateFrames", 0) + extension = "." + repre_entity["context"]["ext"] - if extension in VIDEO_EXTENSIONS: - last += 1 + if extension in VIDEO_EXTENSIONS: + last += slate_frames - files_count = len(repre_entity["files"]) - if extension in IMAGE_EXTENSIONS and files_count != 1: - first -= 1 + files_count = len(repre_entity["files"]) + if extension in IMAGE_EXTENSIONS and files_count != 1: + first -= slate_frames # Fallback to folder name when namespace is None if namespace is None: @@ -181,7 +180,7 @@ class LoadClip(plugin.NukeLoader): ) self._set_range_to_node( - read_node, first, last, start_at_workfile, slate_frame + read_node, first, last, start_at_workfile, slate_frames ) version_name = version_entity["version"] @@ -418,7 +417,7 @@ class LoadClip(plugin.NukeLoader): nuke.delete(member) def _set_range_to_node( - self, read_node, first, last, start_at_workfile, slate_frame=False + self, read_node, first, last, start_at_workfile, slate_frames=0 ): read_node['origfirst'].setValue(int(first)) read_node['first'].setValue(int(first)) @@ -429,9 +428,7 @@ class LoadClip(plugin.NukeLoader): if start_at_workfile: read_node['frame_mode'].setValue("start at") - start_frame = self.script_start - if slate_frame: - start_frame -= 1 + start_frame = self.script_start - slate_frames read_node['frame'].setValue(str(start_frame)) diff --git a/client/ayon_core/hosts/nuke/plugins/publish/extract_slate_frame.py b/client/ayon_core/hosts/nuke/plugins/publish/extract_slate_frame.py index 67d2bd272c..627888ac92 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/extract_slate_frame.py +++ b/client/ayon_core/hosts/nuke/plugins/publish/extract_slate_frame.py @@ -301,7 +301,7 @@ class ExtractSlateFrame(publish.Extractor): "__ matching_repre: {}".format(pformat(matching_repre))) data = matching_repre.get("data", {}) - data["slateFrame"] = True + data["slateFrames"] = 1 matching_repre["data"] = data self.log.info("Added slate frame to representation files") diff --git a/client/ayon_core/plugins/publish/extract_slate_data.py b/client/ayon_core/plugins/publish/extract_slate_data.py index 0a28083f1e..750fb5d60a 100644 --- a/client/ayon_core/plugins/publish/extract_slate_data.py +++ b/client/ayon_core/plugins/publish/extract_slate_data.py @@ -18,5 +18,5 @@ class ExtractSlateData(publish.Extractor): continue data = representation.get("data", {}) - data["slateFrame"] = True + data["slateFrames"] = 1 representation["data"] = data From c193e2e6144c259ee7dbcefaebfe9800c2a0bd3b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 9 Apr 2024 13:19:12 +0200 Subject: [PATCH 51/56] Fix delivery - fixes #351 --- client/ayon_core/plugins/load/delivery.py | 27 +++++++++++++++-------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/client/ayon_core/plugins/load/delivery.py b/client/ayon_core/plugins/load/delivery.py index f8184c8567..5fadf7ea3c 100644 --- a/client/ayon_core/plugins/load/delivery.py +++ b/client/ayon_core/plugins/load/delivery.py @@ -91,9 +91,15 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): longest_key = max(self.templates.keys(), key=len) dropdown.setMinimumContentsLength(len(longest_key)) - template_label = QtWidgets.QLabel() - template_label.setCursor(QtGui.QCursor(QtCore.Qt.IBeamCursor)) - template_label.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse) + template_dir_label = QtWidgets.QLabel() + template_dir_label.setCursor(QtGui.QCursor(QtCore.Qt.IBeamCursor)) + template_dir_label.setTextInteractionFlags( + QtCore.Qt.TextSelectableByMouse) + + template_file_label = QtWidgets.QLabel() + template_file_label.setCursor(QtGui.QCursor(QtCore.Qt.IBeamCursor)) + template_file_label.setTextInteractionFlags( + QtCore.Qt.TextSelectableByMouse) renumber_frame = QtWidgets.QCheckBox() @@ -123,7 +129,8 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): input_layout.addRow("Selected representations", selected_label) input_layout.addRow("Delivery template", dropdown) - input_layout.addRow("Template value", template_label) + input_layout.addRow("Template directory", template_dir_label) + input_layout.addRow("Template file", template_file_label) input_layout.addRow("Renumber Frame", renumber_frame) input_layout.addRow("Renumber start frame", first_frame_start) input_layout.addRow("Root", root_line_edit) @@ -151,7 +158,8 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): layout.addWidget(text_area) self.selected_label = selected_label - self.template_label = template_label + self.template_dir_label = template_dir_label + self.template_file_label = template_file_label self.dropdown = dropdown self.first_frame_start = first_frame_start self.renumber_frame = renumber_frame @@ -282,10 +290,10 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): """Adds list of delivery templates from Anatomy to dropdown.""" templates = {} for template_name, value in anatomy.templates["delivery"].items(): - path_template = value["path"] + directory_template = value["directory"] if ( - not isinstance(path_template, str) - or not path_template.startswith('{root') + not isinstance(directory_template, str) + or not directory_template.startswith('{root') ): continue @@ -350,7 +358,8 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): name = self.dropdown.currentText() template_value = self.templates.get(name) if template_value: - self.template_label.setText(template_value) + self.template_dir_label.setText(template_value["directory"]) + self.template_file_label.setText(template_value["file"]) self.btn_delivery.setEnabled(bool(self._get_selected_repres())) def _update_progress(self, uploaded): From 010655007e1a71a92c783ec6d6c7e1c66c215ec6 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 9 Apr 2024 13:56:17 +0200 Subject: [PATCH 52/56] Update client/ayon_core/plugins/load/delivery.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/plugins/load/delivery.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/load/delivery.py b/client/ayon_core/plugins/load/delivery.py index 5fadf7ea3c..d685649496 100644 --- a/client/ayon_core/plugins/load/delivery.py +++ b/client/ayon_core/plugins/load/delivery.py @@ -129,8 +129,8 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): input_layout.addRow("Selected representations", selected_label) input_layout.addRow("Delivery template", dropdown) - input_layout.addRow("Template directory", template_dir_label) - input_layout.addRow("Template file", template_file_label) + input_layout.addRow("Directory template", template_dir_label) + input_layout.addRow("File template", template_file_label) input_layout.addRow("Renumber Frame", renumber_frame) input_layout.addRow("Renumber start frame", first_frame_start) input_layout.addRow("Root", root_line_edit) From e70651bd3f94742a588883cd20fa98db74bff0f0 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 9 Apr 2024 13:57:38 +0200 Subject: [PATCH 53/56] Skip string check --- client/ayon_core/plugins/load/delivery.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/client/ayon_core/plugins/load/delivery.py b/client/ayon_core/plugins/load/delivery.py index d685649496..f363c6418f 100644 --- a/client/ayon_core/plugins/load/delivery.py +++ b/client/ayon_core/plugins/load/delivery.py @@ -291,10 +291,7 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): templates = {} for template_name, value in anatomy.templates["delivery"].items(): directory_template = value["directory"] - if ( - not isinstance(directory_template, str) - or not directory_template.startswith('{root') - ): + if not directory_template.startswith('{root'): continue templates[template_name] = value From 30460440539306de5f1d2ba6f5b26260feec9b99 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 9 Apr 2024 13:57:49 +0200 Subject: [PATCH 54/56] Cosmetics --- client/ayon_core/plugins/load/delivery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/load/delivery.py b/client/ayon_core/plugins/load/delivery.py index f363c6418f..7ad054b6b9 100644 --- a/client/ayon_core/plugins/load/delivery.py +++ b/client/ayon_core/plugins/load/delivery.py @@ -291,7 +291,7 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): templates = {} for template_name, value in anatomy.templates["delivery"].items(): directory_template = value["directory"] - if not directory_template.startswith('{root'): + if not directory_template.startswith("{root"): continue templates[template_name] = value From ecfb972332fe628dc822d6513344705c50f7e121 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 9 Apr 2024 14:13:10 +0200 Subject: [PATCH 55/56] Log a warning --- client/ayon_core/plugins/load/delivery.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/ayon_core/plugins/load/delivery.py b/client/ayon_core/plugins/load/delivery.py index 7ad054b6b9..e2a283a516 100644 --- a/client/ayon_core/plugins/load/delivery.py +++ b/client/ayon_core/plugins/load/delivery.py @@ -292,6 +292,10 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): for template_name, value in anatomy.templates["delivery"].items(): directory_template = value["directory"] if not directory_template.startswith("{root"): + self.log.warning( + "Skipping template '%s' because directory template does " + "not start with `{root`", template_name + ) continue templates[template_name] = value From 8ec3c80c5fe59f0fa65b0e1aaa80bd2843c628a4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 9 Apr 2024 14:13:49 +0200 Subject: [PATCH 56/56] Log the directory template for better log message --- client/ayon_core/plugins/load/delivery.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/load/delivery.py b/client/ayon_core/plugins/load/delivery.py index e2a283a516..cd358846ea 100644 --- a/client/ayon_core/plugins/load/delivery.py +++ b/client/ayon_core/plugins/load/delivery.py @@ -294,7 +294,8 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): if not directory_template.startswith("{root"): self.log.warning( "Skipping template '%s' because directory template does " - "not start with `{root`", template_name + "not start with `{root` in value: %s", + template_name, directory_template ) continue