From b012b0aaeb21d6ffe1e36e90a9743567694c196d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 22 May 2023 20:14:39 +0800 Subject: [PATCH 01/80] implement review in 3dsmax --- openpype/hosts/max/api/lib.py | 5 +- .../hosts/max/plugins/create/create_review.py | 58 ++++++++ .../max/plugins/publish/collect_review.py | 95 ++++++++++++ .../publish/extract_review_animation.py | 135 ++++++++++++++++++ .../publish/validate_camera_contents.py | 2 +- openpype/plugins/publish/extract_burnin.py | 3 +- openpype/plugins/publish/extract_review.py | 1 + .../defaults/project_settings/global.json | 3 +- 8 files changed, 295 insertions(+), 7 deletions(-) create mode 100644 openpype/hosts/max/plugins/create/create_review.py create mode 100644 openpype/hosts/max/plugins/publish/collect_review.py create mode 100644 openpype/hosts/max/plugins/publish/extract_review_animation.py diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index d9213863b1..c1d1f097bd 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -249,10 +249,7 @@ def reset_frame_range(fps: bool = True): frame_range["handleStart"] ) frame_end_handle = frame_range["frameEnd"] + int(frame_range["handleEnd"]) - frange_cmd = ( - f"animationRange = interval {frame_start_handle} {frame_end_handle}" - ) - rt.execute(frange_cmd) + rt.interval(frame_start_handle, frame_end_handle) set_render_frame_range(frame_start_handle, frame_end_handle) diff --git a/openpype/hosts/max/plugins/create/create_review.py b/openpype/hosts/max/plugins/create/create_review.py new file mode 100644 index 0000000000..9939b2e30e --- /dev/null +++ b/openpype/hosts/max/plugins/create/create_review.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +"""Creator plugin for creating review in Max.""" +from openpype.hosts.max.api import plugin +from openpype.pipeline import CreatedInstance +from openpype.lib import BoolDef, EnumDef, NumberDef + + +class CreateReview(plugin.MaxCreator): + """Review in 3dsMax""" + + identifier = "io.openpype.creators.max.review" + label = "Review" + family = "review" + icon = "video-camera" + + def create(self, subset_name, instance_data, pre_create_data): + + instance_data["imageFormat"] = pre_create_data.get("imageFormat") + instance_data["keepImages"] = pre_create_data.get("keepImages") + instance_data["percentSize"] = pre_create_data.get("percentSize") + instance_data["rndLevel"] = pre_create_data.get("rndLevel") + + _ = super(CreateReview, self).create( + subset_name, + instance_data, + pre_create_data) # type: CreatedInstance + + def get_pre_create_attr_defs(self): + attrs = super(CreateReview, self).get_pre_create_attr_defs() + + image_format_enum = [ + "bmp", "cin", "exr", "jpg", "hdr", "rgb", "png", + "rla", "rpf", "dds", "sgi", "tga", "tif", "vrimg" + ] + + rndLevel_enum = [ + "smoothhighlights", "smooth", "facethighlights", + "facet", "flat", "litwireframe", "wireframe", "box" + ] + + return attrs + [ + BoolDef("keepImages", + label="Keep Image Sequences", + default=False), + EnumDef("imageFormat", + image_format_enum, + default="png", + label="Image Format Options"), + NumberDef("percentSize", + label="Percent of Output", + default=100, + minimum=1, + decimals=0), + EnumDef("rndLevel", + rndLevel_enum, + default="png", + label="Preference") + ] diff --git a/openpype/hosts/max/plugins/publish/collect_review.py b/openpype/hosts/max/plugins/publish/collect_review.py new file mode 100644 index 0000000000..916fc60bcc --- /dev/null +++ b/openpype/hosts/max/plugins/publish/collect_review.py @@ -0,0 +1,95 @@ +# dont forget getting the focal length for burnin +"""Collect Review""" +import pyblish.api + +from pymxs import runtime as rt +from openpype.hosts.max.api.lib import get_all_children +from openpype.lib import BoolDef +from openpype.pipeline.publish import OpenPypePyblishPluginMixin + + +class CollectReview(pyblish.api.InstancePlugin, + OpenPypePyblishPluginMixin): + """Collect Review Data for Preview Animation""" + + order = pyblish.api.CollectorOrder + label = "Collect Review Data" + hosts = ['max'] + families = ["review"] + + def process(self, instance): + nodes = get_all_children( + rt.getNodeByName(instance.data["instance_node"])) + focal_length = None + camera = None + for node in nodes: + if rt.classOf(node) in rt.Camera.classes: + rt.viewport.setCamera(node) + camera = node.name + focal_length = node.fov + + attr_values = self.get_attr_values_from_data(instance.data) + data = { + "review_camera": camera, + "frameStart": instance.context.data["frameStart"], + "frameEnd": instance.context.data["frameEnd"], + "fps": instance.context.data["fps"], + "dspGeometry": attr_values.get("dspGeometry"), + "dspShapes": attr_values.get("dspShapes"), + "dspLights": attr_values.get("dspLights"), + "dspCameras": attr_values.get("dspCameras"), + "dspHelpers": attr_values.get("dspHelpers"), + "dspParticles": attr_values.get("dspParticles"), + "dspBones": attr_values.get("dspBones"), + "dspBkg": attr_values.get("dspBkg"), + "dspGrid": attr_values.get("dspGrid"), + "dspSafeFrame": attr_values.get("dspSafeFrame"), + "dspFrameNums": attr_values.get("dspFrameNums") + } + # Enable ftrack functionality + instance.data.setdefault("families", []).append('ftrack') + + burnin_members = instance.data.setdefault("burninDataMembers", {}) + burnin_members["focalLength"] = focal_length + + self.log.debug(f"data:{data}") + instance.data.update(data) + + @classmethod + def get_attribute_defs(cls): + + return [ + BoolDef("dspGeometry", + label="Geometry", + default=True), + BoolDef("dspShapes", + label="Shapes", + default=False), + BoolDef("dspLights", + label="Lights", + default=False), + BoolDef("dspCameras", + label="Cameras", + default=False), + BoolDef("dspHelpers", + label="Helpers", + default=False), + BoolDef("dspParticles", + label="Particle Systems", + default=True), + BoolDef("dspBones", + label="Bone Objects", + default=False), + BoolDef("dspBkg", + label="Background", + default=True), + BoolDef("dspGrid", + label="Active Grid", + default=False), + BoolDef("dspSafeFrame", + label="Safe Frames", + default=False), + BoolDef("dspFrameNums", + label="Frame Numbers", + default=False) + ] diff --git a/openpype/hosts/max/plugins/publish/extract_review_animation.py b/openpype/hosts/max/plugins/publish/extract_review_animation.py new file mode 100644 index 0000000000..1732a1d69f --- /dev/null +++ b/openpype/hosts/max/plugins/publish/extract_review_animation.py @@ -0,0 +1,135 @@ +import os +import pyblish.api +from openpype.pipeline import publish +from pymxs import runtime as rt + + +class ExtractReviewAnimation(publish.Extractor): + """ + Extract Review by Review Animation + """ + + order = pyblish.api.ExtractorOrder + label = "Extract Review Animation" + hosts = ["max"] + families = ["review"] + + def process(self, instance): + self.log.info("Extracting Review Animation ...") + staging_dir = self.staging_dir(instance) + ext = instance.data.get("imageFormat") + filename = "{0}..{1}".format(instance.name, ext) + start = int(instance.data["frameStart"]) + end = int(instance.data["frameEnd"]) + fps = int(instance.data["fps"]) + filepath = os.path.join(staging_dir, filename) + filepath = filepath.replace("\\", "/") + filenames = self.get_files( + instance.name, start, end, ext) + + self.log.info( + "Writing Review Animation to" + " '%s' to '%s'" % (filename, staging_dir)) + + preview_arg = self.set_preview_arg( + instance, filepath, start, end, fps) + rt.execute(preview_arg) + + tags = ["review"] + if not instance.data.get("keepImages"): + tags.append("delete") + + self.log.info("Performing Extraction ...") + + representation = { + "name": instance.data["imageFormat"], + "ext": instance.data["imageFormat"], + "files": filenames, + "stagingDir": staging_dir, + "frameStart": instance.data["frameStart"], + "frameEnd": instance.data["frameEnd"], + "tags": tags, + "preview": True, + "camera_name": instance.data["review_camera"] + } + self.log.debug(f"{representation}") + + if "representations" not in instance.data: + instance.data["representations"] = [] + instance.data["representations"].append(representation) + + def get_files(self, filename, start, end, ext): + file_list = [] + for frame in range(int(start), int(end) + 1): + actual_name = "{}.{:04}.{}".format( + filename, frame, ext) + file_list.append(actual_name) + + return file_list + + def set_preview_arg(self, instance, filepath, + start, end, fps): + job_args = list() + default_option = f'CreatePreview filename:"{filepath}"' + job_args.append(default_option) + + frame_option = f"outputAVI:false start:{start} end:{end} fps:{fps}" # noqa + job_args.append(frame_option) + rndLevel = instance.data.get("rndLevel") + if rndLevel: + option = f"rndLevel:#{rndLevel}" + job_args.append(option) + percentSize = instance.data.get("percentSize") + if percentSize: + size = int(percentSize) + option = f"percentSize:{size}" + job_args.append(option) + dspGeometry = instance.data.get("dspGeometry") + if dspGeometry: + option = f"dspGeometry:{dspGeometry}" + job_args.append(option) + dspShapes = instance.data.get("dspShapes") + if dspShapes: + option = f"dspShapes:{dspShapes}" + job_args.append(option) + dspLights = instance.data.get("dspLights") + if dspLights: + option = f"dspShapes:{dspLights}" + job_args.append(option) + dspCameras = instance.data.get("dspCameras") + if dspCameras: + option = f"dspCameras:{dspCameras}" + job_args.append(option) + dspHelpers = instance.data.get("dspHelpers") + if dspHelpers: + option = f"dspHelpers:{dspHelpers}" + job_args.append(option) + dspParticles = instance.data.get("dspParticles") + if dspParticles: + option = f"dspParticles:{dspParticles}" + job_args.append(option) + dspBones = instance.data.get("dspBones") + if dspBones: + option = f"dspBones:{dspBones}" + job_args.append(option) + dspBkg = instance.data.get("dspBkg") + if dspBkg: + option = f"dspBkg:{dspBkg}" + job_args.append(option) + dspGrid = instance.data.get("dspGrid") + if dspGrid: + option = f"dspBkg:{dspBkg}" + job_args.append(option) + dspSafeFrame = instance.data.get("dspSafeFrame") + if dspSafeFrame: + option = f"dspSafeFrame:{dspSafeFrame}" + job_args.append(option) + dspFrameNums = instance.data.get("dspFrameNums") + if dspFrameNums: + option = f"dspFrameNums:{dspFrameNums}" + job_args.append(option) + + job_str = " ".join(job_args) + self.log.info(f"{job_str}") + + return job_str diff --git a/openpype/hosts/max/plugins/publish/validate_camera_contents.py b/openpype/hosts/max/plugins/publish/validate_camera_contents.py index c81e28a61f..700966959b 100644 --- a/openpype/hosts/max/plugins/publish/validate_camera_contents.py +++ b/openpype/hosts/max/plugins/publish/validate_camera_contents.py @@ -11,7 +11,7 @@ class ValidateCameraContent(pyblish.api.InstancePlugin): """ order = pyblish.api.ValidatorOrder - families = ["camera"] + families = ["camera", "review"] hosts = ["max"] label = "Camera Contents" camera_type = ["$Free_Camera", "$Target_Camera", diff --git a/openpype/plugins/publish/extract_burnin.py b/openpype/plugins/publish/extract_burnin.py index a12e8d18b4..61961ce4ae 100644 --- a/openpype/plugins/publish/extract_burnin.py +++ b/openpype/plugins/publish/extract_burnin.py @@ -50,7 +50,8 @@ class ExtractBurnin(publish.Extractor): "aftereffects", "photoshop", "flame", - "houdini" + "houdini", + "max" # "resolve" ] diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index 1062683319..8420bd018f 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -45,6 +45,7 @@ class ExtractReview(pyblish.api.InstancePlugin): "maya", "blender", "houdini", + "max" "shell", "hiero", "premiere", diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 75f335f1de..7d5897b925 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -255,7 +255,8 @@ "families": ["review"], "hosts": [ "maya", - "houdini" + "houdini", + "max" ], "task_types": [], "task_names": [], From 8516172dedbf12b6eef5c0b6ae01ce78fec93133 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 23 May 2023 22:50:27 +0800 Subject: [PATCH 02/80] add comma back to the 'max' --- openpype/plugins/publish/extract_review.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index 8420bd018f..d397ce8812 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -45,7 +45,7 @@ class ExtractReview(pyblish.api.InstancePlugin): "maya", "blender", "houdini", - "max" + "max", "shell", "hiero", "premiere", From 930f827036e4905783199e9a04c17576f86c2e18 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 26 May 2023 13:33:41 +0800 Subject: [PATCH 03/80] add thumbnail extractor --- .../max/plugins/publish/extract_thumbnail.py | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 openpype/hosts/max/plugins/publish/extract_thumbnail.py diff --git a/openpype/hosts/max/plugins/publish/extract_thumbnail.py b/openpype/hosts/max/plugins/publish/extract_thumbnail.py new file mode 100644 index 0000000000..5ffeb8c0ca --- /dev/null +++ b/openpype/hosts/max/plugins/publish/extract_thumbnail.py @@ -0,0 +1,114 @@ +import os +import pyblish.api +from openpype.pipeline import publish +from pymxs import runtime as rt + + +class ExtractThumbnail(publish.Extractor): + """ + Extract Thumbnail for Review + """ + + order = pyblish.api.ExtractorOrder + label = "Extract Thumbnail" + hosts = ["max"] + families = ["review"] + + def process(self, instance): + self.log.info("Extracting Thumbnail ...") + staging_dir = self.staging_dir(instance) + filename = "{name}..jpg".format(**instance.data) + filepath = os.path.join(staging_dir, filename) + filepath = filepath.replace("\\", "/") + thumbnail = self.get_filename(instance.name) + + self.log.info( + "Writing Thumbnail to" + " '%s' to '%s'" % (filename, staging_dir)) + + preview_arg = self.set_preview_arg( + instance, filepath) + rt.execute(preview_arg) + + representation = { + "name": "thumbnail", + "ext": "jpg", + "files": thumbnail, + "stagingDir": staging_dir, + "thumbnail": True + } + + self.log.debug(f"{representation}") + + if "representations" not in instance.data: + instance.data["representations"] = [] + instance.data["representations"].append(representation) + + def get_filename(self, filename): + return f"{filename}.0001.jpg" + + def set_preview_arg(self, instance, filepath): + job_args = list() + default_option = f'CreatePreview filename:"{filepath}"' + job_args.append(default_option) + + frame_option = f"outputAVI:false start:1 end:1" # noqa + job_args.append(frame_option) + rndLevel = instance.data.get("rndLevel") + if rndLevel: + option = f"rndLevel:#{rndLevel}" + job_args.append(option) + percentSize = instance.data.get("percentSize") + if percentSize: + size = int(percentSize) + option = f"percentSize:{size}" + job_args.append(option) + dspGeometry = instance.data.get("dspGeometry") + if dspGeometry: + option = f"dspGeometry:{dspGeometry}" + job_args.append(option) + dspShapes = instance.data.get("dspShapes") + if dspShapes: + option = f"dspShapes:{dspShapes}" + job_args.append(option) + dspLights = instance.data.get("dspLights") + if dspLights: + option = f"dspShapes:{dspLights}" + job_args.append(option) + dspCameras = instance.data.get("dspCameras") + if dspCameras: + option = f"dspCameras:{dspCameras}" + job_args.append(option) + dspHelpers = instance.data.get("dspHelpers") + if dspHelpers: + option = f"dspHelpers:{dspHelpers}" + job_args.append(option) + dspParticles = instance.data.get("dspParticles") + if dspParticles: + option = f"dspParticles:{dspParticles}" + job_args.append(option) + dspBones = instance.data.get("dspBones") + if dspBones: + option = f"dspBones:{dspBones}" + job_args.append(option) + dspBkg = instance.data.get("dspBkg") + if dspBkg: + option = f"dspBkg:{dspBkg}" + job_args.append(option) + dspGrid = instance.data.get("dspGrid") + if dspGrid: + option = f"dspBkg:{dspBkg}" + job_args.append(option) + dspSafeFrame = instance.data.get("dspSafeFrame") + if dspSafeFrame: + option = f"dspSafeFrame:{dspSafeFrame}" + job_args.append(option) + dspFrameNums = instance.data.get("dspFrameNums") + if dspFrameNums: + option = f"dspFrameNums:{dspFrameNums}" + job_args.append(option) + + job_str = " ".join(job_args) + self.log.info(f"{job_str}") + + return job_str From 277fd3e3423f636240867d81167f0c2c63e22b48 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 15:09:09 +0800 Subject: [PATCH 04/80] create temp directory for thumbnail --- .../max/plugins/publish/extract_thumbnail.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_thumbnail.py b/openpype/hosts/max/plugins/publish/extract_thumbnail.py index 5ffeb8c0ca..8c78f972f7 100644 --- a/openpype/hosts/max/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/max/plugins/publish/extract_thumbnail.py @@ -1,4 +1,5 @@ import os +import tempfile import pyblish.api from openpype.pipeline import publish from pymxs import runtime as rt @@ -16,15 +17,22 @@ class ExtractThumbnail(publish.Extractor): def process(self, instance): self.log.info("Extracting Thumbnail ...") - staging_dir = self.staging_dir(instance) + + # TODO: Create temp directory for thumbnail + # - this is to avoid "override" of source file + tmp_staging = tempfile.mkdtemp(prefix="pyblish_tmp_") + self.log.debug( + f"Create temp directory {tmp_staging} for thumbnail" + ) + instance.context.data["cleanupFullPaths"].append(tmp_staging) filename = "{name}..jpg".format(**instance.data) - filepath = os.path.join(staging_dir, filename) + filepath = os.path.join(tmp_staging, filename) filepath = filepath.replace("\\", "/") thumbnail = self.get_filename(instance.name) self.log.info( "Writing Thumbnail to" - " '%s' to '%s'" % (filename, staging_dir)) + " '%s' to '%s'" % (filename, tmp_staging)) preview_arg = self.set_preview_arg( instance, filepath) @@ -34,7 +42,7 @@ class ExtractThumbnail(publish.Extractor): "name": "thumbnail", "ext": "jpg", "files": thumbnail, - "stagingDir": staging_dir, + "stagingDir": tmp_staging, "thumbnail": True } From 8abfa57b1be9506fbb3a2599c1317d0f96fca275 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 21 Jun 2023 18:26:43 +0800 Subject: [PATCH 05/80] roy's comment --- .../publish/extract_review_animation.py | 63 +++---------------- 1 file changed, 9 insertions(+), 54 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_review_animation.py b/openpype/hosts/max/plugins/publish/extract_review_animation.py index 1732a1d69f..8d7dfdeea9 100644 --- a/openpype/hosts/max/plugins/publish/extract_review_animation.py +++ b/openpype/hosts/max/plugins/publish/extract_review_animation.py @@ -72,63 +72,18 @@ class ExtractReviewAnimation(publish.Extractor): job_args = list() default_option = f'CreatePreview filename:"{filepath}"' job_args.append(default_option) + options = [ + "rndLevel", "percentSize", "dspGeometry", "dspShapes", + "dspLights", "dspCameras", "dspHelpers", "dspParticles", + "dspBones", "dspBkg", "dspGrid", "dspSafeFrame", "dspFrameNums" + ] + for key in options: + enabled= instance.data.get(key) + if enabled: + job_args.append(f"{key}:{enabled}") frame_option = f"outputAVI:false start:{start} end:{end} fps:{fps}" # noqa job_args.append(frame_option) - rndLevel = instance.data.get("rndLevel") - if rndLevel: - option = f"rndLevel:#{rndLevel}" - job_args.append(option) - percentSize = instance.data.get("percentSize") - if percentSize: - size = int(percentSize) - option = f"percentSize:{size}" - job_args.append(option) - dspGeometry = instance.data.get("dspGeometry") - if dspGeometry: - option = f"dspGeometry:{dspGeometry}" - job_args.append(option) - dspShapes = instance.data.get("dspShapes") - if dspShapes: - option = f"dspShapes:{dspShapes}" - job_args.append(option) - dspLights = instance.data.get("dspLights") - if dspLights: - option = f"dspShapes:{dspLights}" - job_args.append(option) - dspCameras = instance.data.get("dspCameras") - if dspCameras: - option = f"dspCameras:{dspCameras}" - job_args.append(option) - dspHelpers = instance.data.get("dspHelpers") - if dspHelpers: - option = f"dspHelpers:{dspHelpers}" - job_args.append(option) - dspParticles = instance.data.get("dspParticles") - if dspParticles: - option = f"dspParticles:{dspParticles}" - job_args.append(option) - dspBones = instance.data.get("dspBones") - if dspBones: - option = f"dspBones:{dspBones}" - job_args.append(option) - dspBkg = instance.data.get("dspBkg") - if dspBkg: - option = f"dspBkg:{dspBkg}" - job_args.append(option) - dspGrid = instance.data.get("dspGrid") - if dspGrid: - option = f"dspBkg:{dspBkg}" - job_args.append(option) - dspSafeFrame = instance.data.get("dspSafeFrame") - if dspSafeFrame: - option = f"dspSafeFrame:{dspSafeFrame}" - job_args.append(option) - dspFrameNums = instance.data.get("dspFrameNums") - if dspFrameNums: - option = f"dspFrameNums:{dspFrameNums}" - job_args.append(option) - job_str = " ".join(job_args) self.log.info(f"{job_str}") From e2d35dedb961fc5bc0495072be1371ffe3cd22e0 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 21 Jun 2023 18:28:29 +0800 Subject: [PATCH 06/80] hound fix --- openpype/hosts/max/plugins/publish/extract_review_animation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/extract_review_animation.py b/openpype/hosts/max/plugins/publish/extract_review_animation.py index 8d7dfdeea9..940068cc51 100644 --- a/openpype/hosts/max/plugins/publish/extract_review_animation.py +++ b/openpype/hosts/max/plugins/publish/extract_review_animation.py @@ -79,7 +79,7 @@ class ExtractReviewAnimation(publish.Extractor): ] for key in options: - enabled= instance.data.get(key) + enabled = instance.data.get(key) if enabled: job_args.append(f"{key}:{enabled}") frame_option = f"outputAVI:false start:{start} end:{end} fps:{fps}" # noqa From 41dd7e06f951d42ccc1d305647840ba07d545416 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 21 Jun 2023 19:33:40 +0800 Subject: [PATCH 07/80] update the args for review animation --- openpype/hosts/max/plugins/create/create_review.py | 2 +- .../max/plugins/publish/extract_review_animation.py | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/max/plugins/create/create_review.py b/openpype/hosts/max/plugins/create/create_review.py index 9939b2e30e..aec70dbe15 100644 --- a/openpype/hosts/max/plugins/create/create_review.py +++ b/openpype/hosts/max/plugins/create/create_review.py @@ -53,6 +53,6 @@ class CreateReview(plugin.MaxCreator): decimals=0), EnumDef("rndLevel", rndLevel_enum, - default="png", + default="smoothhighlights", label="Preference") ] diff --git a/openpype/hosts/max/plugins/publish/extract_review_animation.py b/openpype/hosts/max/plugins/publish/extract_review_animation.py index 940068cc51..fc70701eba 100644 --- a/openpype/hosts/max/plugins/publish/extract_review_animation.py +++ b/openpype/hosts/max/plugins/publish/extract_review_animation.py @@ -9,7 +9,7 @@ class ExtractReviewAnimation(publish.Extractor): Extract Review by Review Animation """ - order = pyblish.api.ExtractorOrder + order = pyblish.api.ExtractorOrder + 0.001 label = "Extract Review Animation" hosts = ["max"] families = ["review"] @@ -72,8 +72,14 @@ class ExtractReviewAnimation(publish.Extractor): job_args = list() default_option = f'CreatePreview filename:"{filepath}"' job_args.append(default_option) + frame_option = f"outputAVI:false start:{start} end:{end} fps:{fps}" # noqa + job_args.append(frame_option) + rndLevel = instance.data.get("rndLevel") + if rndLevel: + option = f"rndLevel:#{rndLevel}" + job_args.append(option) options = [ - "rndLevel", "percentSize", "dspGeometry", "dspShapes", + "percentSize", "dspGeometry", "dspShapes", "dspLights", "dspCameras", "dspHelpers", "dspParticles", "dspBones", "dspBkg", "dspGrid", "dspSafeFrame", "dspFrameNums" ] @@ -82,8 +88,6 @@ class ExtractReviewAnimation(publish.Extractor): enabled = instance.data.get(key) if enabled: job_args.append(f"{key}:{enabled}") - frame_option = f"outputAVI:false start:{start} end:{end} fps:{fps}" # noqa - job_args.append(frame_option) job_str = " ".join(job_args) self.log.info(f"{job_str}") From 9fd1321cb9860af5bf0f157b1e5a88199ea909b3 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 22 Jun 2023 21:16:14 +0800 Subject: [PATCH 08/80] roy's comment and make sure fps for preview animation in thumbnail extractor is the same as preview animation --- .../publish/extract_review_animation.py | 7 +- .../max/plugins/publish/extract_thumbnail.py | 76 +++++-------------- 2 files changed, 20 insertions(+), 63 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_review_animation.py b/openpype/hosts/max/plugins/publish/extract_review_animation.py index fc70701eba..98ffa5c1d3 100644 --- a/openpype/hosts/max/plugins/publish/extract_review_animation.py +++ b/openpype/hosts/max/plugins/publish/extract_review_animation.py @@ -15,7 +15,6 @@ class ExtractReviewAnimation(publish.Extractor): families = ["review"] def process(self, instance): - self.log.info("Extracting Review Animation ...") staging_dir = self.staging_dir(instance) ext = instance.data.get("imageFormat") filename = "{0}..{1}".format(instance.name, ext) @@ -27,7 +26,7 @@ class ExtractReviewAnimation(publish.Extractor): filenames = self.get_files( instance.name, start, end, ext) - self.log.info( + self.log.debug( "Writing Review Animation to" " '%s' to '%s'" % (filename, staging_dir)) @@ -39,7 +38,7 @@ class ExtractReviewAnimation(publish.Extractor): if not instance.data.get("keepImages"): tags.append("delete") - self.log.info("Performing Extraction ...") + self.log.debug("Performing Extraction ...") representation = { "name": instance.data["imageFormat"], @@ -89,6 +88,6 @@ class ExtractReviewAnimation(publish.Extractor): if enabled: job_args.append(f"{key}:{enabled}") job_str = " ".join(job_args) - self.log.info(f"{job_str}") + self.log.debug(job_str) return job_str diff --git a/openpype/hosts/max/plugins/publish/extract_thumbnail.py b/openpype/hosts/max/plugins/publish/extract_thumbnail.py index 8c78f972f7..3f3a804250 100644 --- a/openpype/hosts/max/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/max/plugins/publish/extract_thumbnail.py @@ -16,26 +16,25 @@ class ExtractThumbnail(publish.Extractor): families = ["review"] def process(self, instance): - self.log.info("Extracting Thumbnail ...") - # TODO: Create temp directory for thumbnail # - this is to avoid "override" of source file tmp_staging = tempfile.mkdtemp(prefix="pyblish_tmp_") self.log.debug( f"Create temp directory {tmp_staging} for thumbnail" ) + fps = int(instance.data["fps"]) instance.context.data["cleanupFullPaths"].append(tmp_staging) - filename = "{name}..jpg".format(**instance.data) + filename = "{name}_thumbnail..jpg".format(**instance.data) filepath = os.path.join(tmp_staging, filename) filepath = filepath.replace("\\", "/") thumbnail = self.get_filename(instance.name) - self.log.info( + self.log.debug( "Writing Thumbnail to" " '%s' to '%s'" % (filename, tmp_staging)) preview_arg = self.set_preview_arg( - instance, filepath) + instance, filepath, fps) rt.execute(preview_arg) representation = { @@ -53,70 +52,29 @@ class ExtractThumbnail(publish.Extractor): instance.data["representations"].append(representation) def get_filename(self, filename): - return f"{filename}.0001.jpg" + return f"{filename}_thumbnail.0001.jpg" - def set_preview_arg(self, instance, filepath): + def set_preview_arg(self, instance, filepath, fps): job_args = list() default_option = f'CreatePreview filename:"{filepath}"' job_args.append(default_option) - - frame_option = f"outputAVI:false start:1 end:1" # noqa + frame_option = f"outputAVI:false start:1 end:1 fps:{fps}" # noqa job_args.append(frame_option) rndLevel = instance.data.get("rndLevel") if rndLevel: option = f"rndLevel:#{rndLevel}" job_args.append(option) - percentSize = instance.data.get("percentSize") - if percentSize: - size = int(percentSize) - option = f"percentSize:{size}" - job_args.append(option) - dspGeometry = instance.data.get("dspGeometry") - if dspGeometry: - option = f"dspGeometry:{dspGeometry}" - job_args.append(option) - dspShapes = instance.data.get("dspShapes") - if dspShapes: - option = f"dspShapes:{dspShapes}" - job_args.append(option) - dspLights = instance.data.get("dspLights") - if dspLights: - option = f"dspShapes:{dspLights}" - job_args.append(option) - dspCameras = instance.data.get("dspCameras") - if dspCameras: - option = f"dspCameras:{dspCameras}" - job_args.append(option) - dspHelpers = instance.data.get("dspHelpers") - if dspHelpers: - option = f"dspHelpers:{dspHelpers}" - job_args.append(option) - dspParticles = instance.data.get("dspParticles") - if dspParticles: - option = f"dspParticles:{dspParticles}" - job_args.append(option) - dspBones = instance.data.get("dspBones") - if dspBones: - option = f"dspBones:{dspBones}" - job_args.append(option) - dspBkg = instance.data.get("dspBkg") - if dspBkg: - option = f"dspBkg:{dspBkg}" - job_args.append(option) - dspGrid = instance.data.get("dspGrid") - if dspGrid: - option = f"dspBkg:{dspBkg}" - job_args.append(option) - dspSafeFrame = instance.data.get("dspSafeFrame") - if dspSafeFrame: - option = f"dspSafeFrame:{dspSafeFrame}" - job_args.append(option) - dspFrameNums = instance.data.get("dspFrameNums") - if dspFrameNums: - option = f"dspFrameNums:{dspFrameNums}" - job_args.append(option) + options = [ + "percentSize", "dspGeometry", "dspShapes", + "dspLights", "dspCameras", "dspHelpers", "dspParticles", + "dspBones", "dspBkg", "dspGrid", "dspSafeFrame", "dspFrameNums" + ] + for key in options: + enabled = instance.data.get(key) + if enabled: + job_args.append(f"{key}:{enabled}") job_str = " ".join(job_args) - self.log.info(f"{job_str}") + self.log.debug(job_str) return job_str From 45f02e8db3ed462b914c846de7042803f446da30 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 22 Jun 2023 22:54:15 +0800 Subject: [PATCH 09/80] roy's comment --- openpype/hosts/max/api/lib.py | 10 ++++++++++ openpype/hosts/max/plugins/create/create_review.py | 4 ++-- .../hosts/max/plugins/publish/collect_review.py | 7 +++---- .../max/plugins/publish/extract_review_animation.py | 13 ++++++++----- .../hosts/max/plugins/publish/extract_thumbnail.py | 7 ++++--- 5 files changed, 27 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 161c73e9a4..83bc597be2 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -282,3 +282,13 @@ def get_max_version(): """ max_info = rt.MaxVersion() return max_info[7] + +@contextlib.contextmanager +def viewport_camera(camera): + original = rt.viewport.getCamera() + review_camera = rt.getNodeByName(camera) + try: + rt.viewport.setCamera(review_camera) + yield + finally: + rt.viewport.setCamera(original) diff --git a/openpype/hosts/max/plugins/create/create_review.py b/openpype/hosts/max/plugins/create/create_review.py index aec70dbe15..d5fc31ce50 100644 --- a/openpype/hosts/max/plugins/create/create_review.py +++ b/openpype/hosts/max/plugins/create/create_review.py @@ -20,10 +20,10 @@ class CreateReview(plugin.MaxCreator): instance_data["percentSize"] = pre_create_data.get("percentSize") instance_data["rndLevel"] = pre_create_data.get("rndLevel") - _ = super(CreateReview, self).create( + super(CreateReview, self).create( subset_name, instance_data, - pre_create_data) # type: CreatedInstance + pre_create_data) def get_pre_create_attr_defs(self): attrs = super(CreateReview, self).get_pre_create_attr_defs() diff --git a/openpype/hosts/max/plugins/publish/collect_review.py b/openpype/hosts/max/plugins/publish/collect_review.py index b2a187116a..7aeb45f46b 100644 --- a/openpype/hosts/max/plugins/publish/collect_review.py +++ b/openpype/hosts/max/plugins/publish/collect_review.py @@ -19,16 +19,15 @@ class CollectReview(pyblish.api.InstancePlugin, def process(self, instance): nodes = instance.data["members"] focal_length = None - camera = None + camera_name = None for node in nodes: if rt.classOf(node) in rt.Camera.classes: - rt.viewport.setCamera(node) - camera = node.name + camera_name = node.name focal_length = node.fov attr_values = self.get_attr_values_from_data(instance.data) data = { - "review_camera": camera, + "review_camera": camera_name, "frameStart": instance.context.data["frameStart"], "frameEnd": instance.context.data["frameEnd"], "fps": instance.context.data["fps"], diff --git a/openpype/hosts/max/plugins/publish/extract_review_animation.py b/openpype/hosts/max/plugins/publish/extract_review_animation.py index 98ffa5c1d3..4e19daddf4 100644 --- a/openpype/hosts/max/plugins/publish/extract_review_animation.py +++ b/openpype/hosts/max/plugins/publish/extract_review_animation.py @@ -1,7 +1,8 @@ import os import pyblish.api -from openpype.pipeline import publish from pymxs import runtime as rt +from openpype.pipeline import publish +from openpype.hosts.max.api.lib import viewport_camera class ExtractReviewAnimation(publish.Extractor): @@ -30,9 +31,11 @@ class ExtractReviewAnimation(publish.Extractor): "Writing Review Animation to" " '%s' to '%s'" % (filename, staging_dir)) - preview_arg = self.set_preview_arg( - instance, filepath, start, end, fps) - rt.execute(preview_arg) + review_camera = instance.data["review_camera"] + with viewport_camera(review_camera): + preview_arg = self.set_preview_arg( + instance, filepath, start, end, fps) + rt.execute(preview_arg) tags = ["review"] if not instance.data.get("keepImages"): @@ -49,7 +52,7 @@ class ExtractReviewAnimation(publish.Extractor): "frameEnd": instance.data["frameEnd"], "tags": tags, "preview": True, - "camera_name": instance.data["review_camera"] + "camera_name": review_camera } self.log.debug(f"{representation}") diff --git a/openpype/hosts/max/plugins/publish/extract_thumbnail.py b/openpype/hosts/max/plugins/publish/extract_thumbnail.py index 3f3a804250..faa09bdad9 100644 --- a/openpype/hosts/max/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/max/plugins/publish/extract_thumbnail.py @@ -23,6 +23,7 @@ class ExtractThumbnail(publish.Extractor): f"Create temp directory {tmp_staging} for thumbnail" ) fps = int(instance.data["fps"]) + frame = int(instance.data["frameStart"]) instance.context.data["cleanupFullPaths"].append(tmp_staging) filename = "{name}_thumbnail..jpg".format(**instance.data) filepath = os.path.join(tmp_staging, filename) @@ -34,7 +35,7 @@ class ExtractThumbnail(publish.Extractor): " '%s' to '%s'" % (filename, tmp_staging)) preview_arg = self.set_preview_arg( - instance, filepath, fps) + instance, filepath, fps, frame) rt.execute(preview_arg) representation = { @@ -54,11 +55,11 @@ class ExtractThumbnail(publish.Extractor): def get_filename(self, filename): return f"{filename}_thumbnail.0001.jpg" - def set_preview_arg(self, instance, filepath, fps): + def set_preview_arg(self, instance, filepath, fps, frame): job_args = list() default_option = f'CreatePreview filename:"{filepath}"' job_args.append(default_option) - frame_option = f"outputAVI:false start:1 end:1 fps:{fps}" # noqa + frame_option = f"outputAVI:false start:{frame} end:{frame} fps:{fps}" # noqa job_args.append(frame_option) rndLevel = instance.data.get("rndLevel") if rndLevel: From 5210d9bde3e787929d1bf319dd3ef7532261981d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 22 Jun 2023 22:55:24 +0800 Subject: [PATCH 10/80] hound fix --- openpype/hosts/max/api/lib.py | 1 + openpype/hosts/max/plugins/create/create_review.py | 1 - openpype/hosts/max/plugins/publish/collect_review.py | 3 +++ openpype/hosts/max/plugins/publish/extract_thumbnail.py | 1 + 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 83bc597be2..88f1b35a14 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -283,6 +283,7 @@ def get_max_version(): max_info = rt.MaxVersion() return max_info[7] + @contextlib.contextmanager def viewport_camera(camera): original = rt.viewport.getCamera() diff --git a/openpype/hosts/max/plugins/create/create_review.py b/openpype/hosts/max/plugins/create/create_review.py index d5fc31ce50..5737114fcc 100644 --- a/openpype/hosts/max/plugins/create/create_review.py +++ b/openpype/hosts/max/plugins/create/create_review.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- """Creator plugin for creating review in Max.""" from openpype.hosts.max.api import plugin -from openpype.pipeline import CreatedInstance from openpype.lib import BoolDef, EnumDef, NumberDef diff --git a/openpype/hosts/max/plugins/publish/collect_review.py b/openpype/hosts/max/plugins/publish/collect_review.py index 7aeb45f46b..5b01c7ddf7 100644 --- a/openpype/hosts/max/plugins/publish/collect_review.py +++ b/openpype/hosts/max/plugins/publish/collect_review.py @@ -20,13 +20,16 @@ class CollectReview(pyblish.api.InstancePlugin, nodes = instance.data["members"] focal_length = None camera_name = None + camera = None for node in nodes: if rt.classOf(node) in rt.Camera.classes: + camera = node camera_name = node.name focal_length = node.fov attr_values = self.get_attr_values_from_data(instance.data) data = { + "camera_node": camera, "review_camera": camera_name, "frameStart": instance.context.data["frameStart"], "frameEnd": instance.context.data["frameEnd"], diff --git a/openpype/hosts/max/plugins/publish/extract_thumbnail.py b/openpype/hosts/max/plugins/publish/extract_thumbnail.py index faa09bdad9..8de15a00d4 100644 --- a/openpype/hosts/max/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/max/plugins/publish/extract_thumbnail.py @@ -14,6 +14,7 @@ class ExtractThumbnail(publish.Extractor): label = "Extract Thumbnail" hosts = ["max"] families = ["review"] + start def process(self, instance): # TODO: Create temp directory for thumbnail From 173845859e171d86443e596af44f8caec0482722 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 22 Jun 2023 22:56:41 +0800 Subject: [PATCH 11/80] hound fix --- openpype/hosts/max/plugins/publish/extract_thumbnail.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/extract_thumbnail.py b/openpype/hosts/max/plugins/publish/extract_thumbnail.py index 8de15a00d4..faa09bdad9 100644 --- a/openpype/hosts/max/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/max/plugins/publish/extract_thumbnail.py @@ -14,7 +14,6 @@ class ExtractThumbnail(publish.Extractor): label = "Extract Thumbnail" hosts = ["max"] families = ["review"] - start def process(self, instance): # TODO: Create temp directory for thumbnail From 991bcec434c6d26bd0618c2193839af21ac26995 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 23 Jun 2023 15:26:57 +0800 Subject: [PATCH 12/80] add viewport_function into thumbnail extractor --- .../hosts/max/plugins/publish/extract_thumbnail.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_thumbnail.py b/openpype/hosts/max/plugins/publish/extract_thumbnail.py index faa09bdad9..33f705fefd 100644 --- a/openpype/hosts/max/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/max/plugins/publish/extract_thumbnail.py @@ -1,8 +1,9 @@ import os import tempfile import pyblish.api -from openpype.pipeline import publish from pymxs import runtime as rt +from openpype.pipeline import publish +from openpype.hosts.max.api.lib import viewport_camera class ExtractThumbnail(publish.Extractor): @@ -33,10 +34,11 @@ class ExtractThumbnail(publish.Extractor): self.log.debug( "Writing Thumbnail to" " '%s' to '%s'" % (filename, tmp_staging)) - - preview_arg = self.set_preview_arg( - instance, filepath, fps, frame) - rt.execute(preview_arg) + review_camera = instance.data["review_camera"] + with viewport_camera(review_camera): + preview_arg = self.set_preview_arg( + instance, filepath, fps, frame) + rt.execute(preview_arg) representation = { "name": "thumbnail", From ab1cdf5cef7bff2895f1cd0ca132f81f66b64bdd Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 23 Jun 2023 17:14:07 +0800 Subject: [PATCH 13/80] add validation to make sure there must be camera instance included --- .../hosts/max/plugins/publish/validate_camera_contents.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/hosts/max/plugins/publish/validate_camera_contents.py b/openpype/hosts/max/plugins/publish/validate_camera_contents.py index 0c61e6431d..4a09f415e1 100644 --- a/openpype/hosts/max/plugins/publish/validate_camera_contents.py +++ b/openpype/hosts/max/plugins/publish/validate_camera_contents.py @@ -18,6 +18,10 @@ class ValidateCameraContent(pyblish.api.InstancePlugin): "$Physical_Camera", "$Target"] def process(self, instance): + selection_list = instance.data["members"] + if not selection_list: + raise PublishValidationError("No camera instance found..") + invalid = self.get_invalid(instance) if invalid: raise PublishValidationError(("Camera instance must only include" From 44482c51a673cc63b9f3009fe4490956dce28b6e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 23 Jun 2023 17:30:17 +0800 Subject: [PATCH 14/80] add review instance into no max content --- .../hosts/max/plugins/publish/validate_camera_contents.py | 4 ---- openpype/hosts/max/plugins/publish/validate_no_max_content.py | 3 ++- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_camera_contents.py b/openpype/hosts/max/plugins/publish/validate_camera_contents.py index 4a09f415e1..0c61e6431d 100644 --- a/openpype/hosts/max/plugins/publish/validate_camera_contents.py +++ b/openpype/hosts/max/plugins/publish/validate_camera_contents.py @@ -18,10 +18,6 @@ class ValidateCameraContent(pyblish.api.InstancePlugin): "$Physical_Camera", "$Target"] def process(self, instance): - selection_list = instance.data["members"] - if not selection_list: - raise PublishValidationError("No camera instance found..") - invalid = self.get_invalid(instance) if invalid: raise PublishValidationError(("Camera instance must only include" diff --git a/openpype/hosts/max/plugins/publish/validate_no_max_content.py b/openpype/hosts/max/plugins/publish/validate_no_max_content.py index ba4a6882c2..c6a27dace3 100644 --- a/openpype/hosts/max/plugins/publish/validate_no_max_content.py +++ b/openpype/hosts/max/plugins/publish/validate_no_max_content.py @@ -13,7 +13,8 @@ class ValidateMaxContents(pyblish.api.InstancePlugin): order = pyblish.api.ValidatorOrder families = ["camera", "maxScene", - "maxrender"] + "maxrender", + "review"] hosts = ["max"] label = "Max Scene Contents" From c2faeb251f6789701d2d9b1bdcedad397432bbc1 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 23 Jun 2023 17:33:59 +0800 Subject: [PATCH 15/80] remove adding unused instance data --- openpype/hosts/max/plugins/publish/collect_review.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/collect_review.py b/openpype/hosts/max/plugins/publish/collect_review.py index 5b01c7ddf7..7aeb45f46b 100644 --- a/openpype/hosts/max/plugins/publish/collect_review.py +++ b/openpype/hosts/max/plugins/publish/collect_review.py @@ -20,16 +20,13 @@ class CollectReview(pyblish.api.InstancePlugin, nodes = instance.data["members"] focal_length = None camera_name = None - camera = None for node in nodes: if rt.classOf(node) in rt.Camera.classes: - camera = node camera_name = node.name focal_length = node.fov attr_values = self.get_attr_values_from_data(instance.data) data = { - "camera_node": camera, "review_camera": camera_name, "frameStart": instance.context.data["frameStart"], "frameEnd": instance.context.data["frameEnd"], From c00f1b6449a0afdf636a776fa3c8eaee7f8e96d5 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Sat, 24 Jun 2023 13:54:10 +0800 Subject: [PATCH 16/80] remove hardcoded part from thumbnail extractor and add timeline validator for max review --- openpype/hosts/max/api/lib.py | 13 ++++- .../max/plugins/publish/extract_thumbnail.py | 11 +++-- .../publish/validate_animation_timeline.py | 47 +++++++++++++++++++ 3 files changed, 66 insertions(+), 5 deletions(-) create mode 100644 openpype/hosts/max/plugins/publish/validate_animation_timeline.py diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 88f1b35a14..995c35792a 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -250,7 +250,7 @@ def reset_frame_range(fps: bool = True): frame_range["handleStart"] ) frame_end_handle = frame_range["frameEnd"] + int(frame_range["handleEnd"]) - rt.interval(frame_start_handle, frame_end_handle) + set_timeline(frame_start_handle, frame_end_handle) set_render_frame_range(frame_start_handle, frame_end_handle) @@ -287,9 +287,20 @@ def get_max_version(): @contextlib.contextmanager def viewport_camera(camera): original = rt.viewport.getCamera() + if not original: + # if there is no original camera + # use the current camera as original + original = rt.getNodeByName(camera) review_camera = rt.getNodeByName(camera) try: rt.viewport.setCamera(review_camera) yield finally: rt.viewport.setCamera(original) + + +def set_timeline(frameStart, frameEnd): + """Set frame range for timeline editor in Max + """ + rt.animationRange = rt.interval(frameStart, frameEnd) + return rt.animationRange diff --git a/openpype/hosts/max/plugins/publish/extract_thumbnail.py b/openpype/hosts/max/plugins/publish/extract_thumbnail.py index 33f705fefd..cc943f388f 100644 --- a/openpype/hosts/max/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/max/plugins/publish/extract_thumbnail.py @@ -29,7 +29,7 @@ class ExtractThumbnail(publish.Extractor): filename = "{name}_thumbnail..jpg".format(**instance.data) filepath = os.path.join(tmp_staging, filename) filepath = filepath.replace("\\", "/") - thumbnail = self.get_filename(instance.name) + thumbnail = self.get_filename(instance.name, frame) self.log.debug( "Writing Thumbnail to" @@ -42,7 +42,7 @@ class ExtractThumbnail(publish.Extractor): representation = { "name": "thumbnail", - "ext": "jpg", + "ext": "png", "files": thumbnail, "stagingDir": tmp_staging, "thumbnail": True @@ -54,8 +54,11 @@ class ExtractThumbnail(publish.Extractor): instance.data["representations"] = [] instance.data["representations"].append(representation) - def get_filename(self, filename): - return f"{filename}_thumbnail.0001.jpg" + def get_filename(self, filename, target_frame): + thumbnail_name = "{}_thumbnail.{:04}.png".format( + filename, target_frame + ) + return thumbnail_name def set_preview_arg(self, instance, filepath, fps, frame): job_args = list() diff --git a/openpype/hosts/max/plugins/publish/validate_animation_timeline.py b/openpype/hosts/max/plugins/publish/validate_animation_timeline.py new file mode 100644 index 0000000000..249451680c --- /dev/null +++ b/openpype/hosts/max/plugins/publish/validate_animation_timeline.py @@ -0,0 +1,47 @@ +import pyblish.api + +from pymxs import runtime as rt +from openpype.pipeline.publish import ( + RepairAction, + ValidateContentsOrder, + PublishValidationError +) +from openpype.hosts.max.api.lib import get_frame_range, set_timeline + + +class ValidateAnimationTimeline(pyblish.api.InstancePlugin): + """ + Validates Animation Timeline for Preview Animation in Max + """ + + label = "Animation Timeline for Review" + order = ValidateContentsOrder + families = ["review"] + hosts = ["max"] + actions = [RepairAction] + + def process(self, instance): + frame_range = get_frame_range() + frame_start_handle = frame_range["frameStart"] - int( + frame_range["handleStart"] + ) + frame_end_handle = frame_range["frameEnd"] + int( + frame_range["handleEnd"] + ) + if rt.animationRange != rt.interval( + frame_start_handle, frame_end_handle): + raise PublishValidationError("Incorrect animation timeline" + "set for preview animation.. " + "\nYou can use repair action to " + "the correct animation timeline") + + @classmethod + def repair(cls, instance): + frame_range = get_frame_range() + frame_start_handle = frame_range["frameStart"] - int( + frame_range["handleStart"] + ) + frame_end_handle = frame_range["frameEnd"] + int( + frame_range["handleEnd"] + ) + set_timeline(frame_start_handle, frame_end_handle) From 5abbceeace24f3b183507d6d91f8aa7a4cb6bd95 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Sat, 24 Jun 2023 13:55:20 +0800 Subject: [PATCH 17/80] hound fix --- .../hosts/max/plugins/publish/validate_animation_timeline.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_animation_timeline.py b/openpype/hosts/max/plugins/publish/validate_animation_timeline.py index 249451680c..8e49c94aa7 100644 --- a/openpype/hosts/max/plugins/publish/validate_animation_timeline.py +++ b/openpype/hosts/max/plugins/publish/validate_animation_timeline.py @@ -28,8 +28,8 @@ class ValidateAnimationTimeline(pyblish.api.InstancePlugin): frame_end_handle = frame_range["frameEnd"] + int( frame_range["handleEnd"] ) - if rt.animationRange != rt.interval( - frame_start_handle, frame_end_handle): + if rt.animationRange != rt.interval(frame_start_handle, + frame_end_handle): raise PublishValidationError("Incorrect animation timeline" "set for preview animation.. " "\nYou can use repair action to " From 0d590cb5728e37087c1dcbadbc4a47abe0b0038b Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Sat, 24 Jun 2023 17:17:42 +0800 Subject: [PATCH 18/80] viewer not popup in the max2024 but will popup in max 2023 --- .../max/plugins/publish/extract_review_animation.py | 8 +++++++- openpype/hosts/max/plugins/publish/extract_thumbnail.py | 9 +++++++-- .../max/plugins/publish/validate_animation_timeline.py | 7 ++++--- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_review_animation.py b/openpype/hosts/max/plugins/publish/extract_review_animation.py index 4e19daddf4..8e06e52b5c 100644 --- a/openpype/hosts/max/plugins/publish/extract_review_animation.py +++ b/openpype/hosts/max/plugins/publish/extract_review_animation.py @@ -2,7 +2,7 @@ import os import pyblish.api from pymxs import runtime as rt from openpype.pipeline import publish -from openpype.hosts.max.api.lib import viewport_camera +from openpype.hosts.max.api.lib import viewport_camera, get_max_version class ExtractReviewAnimation(publish.Extractor): @@ -90,6 +90,12 @@ class ExtractReviewAnimation(publish.Extractor): enabled = instance.data.get(key) if enabled: job_args.append(f"{key}:{enabled}") + + if get_max_version() == 2024: + # hardcoded for current stage + auto_play_option = "autoPlay:false" + job_args.append(auto_play_option) + job_str = " ".join(job_args) self.log.debug(job_str) diff --git a/openpype/hosts/max/plugins/publish/extract_thumbnail.py b/openpype/hosts/max/plugins/publish/extract_thumbnail.py index cc943f388f..82f4fc7a8b 100644 --- a/openpype/hosts/max/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/max/plugins/publish/extract_thumbnail.py @@ -3,7 +3,7 @@ import tempfile import pyblish.api from pymxs import runtime as rt from openpype.pipeline import publish -from openpype.hosts.max.api.lib import viewport_camera +from openpype.hosts.max.api.lib import viewport_camera, get_max_version class ExtractThumbnail(publish.Extractor): @@ -26,7 +26,7 @@ class ExtractThumbnail(publish.Extractor): fps = int(instance.data["fps"]) frame = int(instance.data["frameStart"]) instance.context.data["cleanupFullPaths"].append(tmp_staging) - filename = "{name}_thumbnail..jpg".format(**instance.data) + filename = "{name}_thumbnail..png".format(**instance.data) filepath = os.path.join(tmp_staging, filename) filepath = filepath.replace("\\", "/") thumbnail = self.get_filename(instance.name, frame) @@ -80,6 +80,11 @@ class ExtractThumbnail(publish.Extractor): enabled = instance.data.get(key) if enabled: job_args.append(f"{key}:{enabled}") + if get_max_version() == 2024: + # hardcoded for current stage + auto_play_option = "autoPlay:false" + job_args.append(auto_play_option) + job_str = " ".join(job_args) self.log.debug(job_str) diff --git a/openpype/hosts/max/plugins/publish/validate_animation_timeline.py b/openpype/hosts/max/plugins/publish/validate_animation_timeline.py index 8e49c94aa7..2a9483c763 100644 --- a/openpype/hosts/max/plugins/publish/validate_animation_timeline.py +++ b/openpype/hosts/max/plugins/publish/validate_animation_timeline.py @@ -28,9 +28,10 @@ class ValidateAnimationTimeline(pyblish.api.InstancePlugin): frame_end_handle = frame_range["frameEnd"] + int( frame_range["handleEnd"] ) - if rt.animationRange != rt.interval(frame_start_handle, - frame_end_handle): - raise PublishValidationError("Incorrect animation timeline" + if rt.animationRange.start != frame_start_handle or ( + rt.animationRange.end != frame_end_handle + ): + raise PublishValidationError("Incorrect animation timeline " "set for preview animation.. " "\nYou can use repair action to " "the correct animation timeline") From 1442bddbcc134e48b69ab3a48fc829599d393d02 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 26 Jun 2023 21:33:02 +0800 Subject: [PATCH 19/80] no duplicates in node reference in OpenpypeData --- openpype/hosts/max/api/plugin.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/hosts/max/api/plugin.py b/openpype/hosts/max/api/plugin.py index 71a0b94e1f..b1a7992ac8 100644 --- a/openpype/hosts/max/api/plugin.py +++ b/openpype/hosts/max/api/plugin.py @@ -42,6 +42,10 @@ MS_CUSTOM_ATTRIB = """attributes "openPypeData" ( handle_name = node_to_name c node_ref = NodeTransformMonitor node:c + idx = finditem list_node.items handle_name + if idx do ( + return False + ) append temp_arr handle_name append i_node_arr node_ref ) From 06ba35fb6c6f8b16b054d39a41ff6d30354e1e8d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 26 Jun 2023 22:00:54 +0800 Subject: [PATCH 20/80] roy's comment --- openpype/hosts/max/api/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/api/plugin.py b/openpype/hosts/max/api/plugin.py index b1a7992ac8..14b0653f40 100644 --- a/openpype/hosts/max/api/plugin.py +++ b/openpype/hosts/max/api/plugin.py @@ -44,7 +44,7 @@ MS_CUSTOM_ATTRIB = """attributes "openPypeData" node_ref = NodeTransformMonitor node:c idx = finditem list_node.items handle_name if idx do ( - return False + continue ) append temp_arr handle_name append i_node_arr node_ref From a3f519fd46089f0cb507a999ea85d78ab8a637d9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 27 Jun 2023 18:03:16 +0200 Subject: [PATCH 21/80] fixing current project in otio export --- openpype/hosts/hiero/api/otio/hiero_export.py | 83 ++++++++----------- 1 file changed, 35 insertions(+), 48 deletions(-) diff --git a/openpype/hosts/hiero/api/otio/hiero_export.py b/openpype/hosts/hiero/api/otio/hiero_export.py index 81cb43fa12..e6d6196196 100644 --- a/openpype/hosts/hiero/api/otio/hiero_export.py +++ b/openpype/hosts/hiero/api/otio/hiero_export.py @@ -10,23 +10,23 @@ from . import utils import hiero.core import hiero.ui -self = sys.modules[__name__] -self.track_types = { - hiero.core.VideoTrack: otio.schema.TrackKind.Video, - hiero.core.AudioTrack: otio.schema.TrackKind.Audio -} -self.project_fps = None -self.marker_color_map = { - "magenta": otio.schema.MarkerColor.MAGENTA, - "red": otio.schema.MarkerColor.RED, - "yellow": otio.schema.MarkerColor.YELLOW, - "green": otio.schema.MarkerColor.GREEN, - "cyan": otio.schema.MarkerColor.CYAN, - "blue": otio.schema.MarkerColor.BLUE, -} -self.timeline = None -self.include_tags = True +class CTX: + track_types = { + hiero.core.VideoTrack: otio.schema.TrackKind.Video, + hiero.core.AudioTrack: otio.schema.TrackKind.Audio + } + project_fps = None + marker_color_map = { + "magenta": otio.schema.MarkerColor.MAGENTA, + "red": otio.schema.MarkerColor.RED, + "yellow": otio.schema.MarkerColor.YELLOW, + "green": otio.schema.MarkerColor.GREEN, + "cyan": otio.schema.MarkerColor.CYAN, + "blue": otio.schema.MarkerColor.BLUE, + } + timeline = None + include_tags = True def flatten(_list): for item in _list: @@ -37,19 +37,6 @@ def flatten(_list): yield item -def get_current_hiero_project(remove_untitled=False): - projects = flatten(hiero.core.projects()) - if not remove_untitled: - return next(iter(projects)) - - # if remove_untitled - for proj in projects: - if "Untitled" in proj.name(): - proj.close() - else: - return proj - - def create_otio_rational_time(frame, fps): return otio.opentime.RationalTime( float(frame), @@ -152,7 +139,7 @@ def create_otio_reference(clip): file_head = media_source.filenameHead() is_sequence = not media_source.singleFile() frame_duration = media_source.duration() - fps = utils.get_rate(clip) or self.project_fps + fps = utils.get_rate(clip) or CTX.project_fps extension = os.path.splitext(path)[-1] if is_sequence: @@ -217,8 +204,8 @@ def get_marker_color(tag): res = re.search(pat, icon) if res: color = res.groupdict().get('color') - if color.lower() in self.marker_color_map: - return self.marker_color_map[color.lower()] + if color.lower() in CTX.marker_color_map: + return CTX.marker_color_map[color.lower()] return otio.schema.MarkerColor.RED @@ -232,7 +219,7 @@ def create_otio_markers(otio_item, item): # Hiero adds this tag to a lot of clips continue - frame_rate = utils.get_rate(item) or self.project_fps + frame_rate = utils.get_rate(item) or CTX.project_fps marked_range = otio.opentime.TimeRange( start_time=otio.opentime.RationalTime( @@ -279,7 +266,7 @@ def create_otio_clip(track_item): duration = int(track_item.duration()) - fps = utils.get_rate(track_item) or self.project_fps + fps = utils.get_rate(track_item) or CTX.project_fps name = track_item.name() media_reference = create_otio_reference(clip) @@ -296,7 +283,7 @@ def create_otio_clip(track_item): ) # Add tags as markers - if self.include_tags: + if CTX.include_tags: create_otio_markers(otio_clip, track_item) create_otio_markers(otio_clip, track_item.source()) @@ -319,13 +306,13 @@ def create_otio_gap(gap_start, clip_start, tl_start_frame, fps): def _create_otio_timeline(): - project = get_current_hiero_project(remove_untitled=False) - metadata = _get_metadata(self.timeline) + project = CTX.timeline.project() + metadata = _get_metadata(CTX.timeline) metadata.update({ - "openpype.timeline.width": int(self.timeline.format().width()), - "openpype.timeline.height": int(self.timeline.format().height()), - "openpype.timeline.pixelAspect": int(self.timeline.format().pixelAspect()), # noqa + "openpype.timeline.width": int(CTX.timeline.format().width()), + "openpype.timeline.height": int(CTX.timeline.format().height()), + "openpype.timeline.pixelAspect": int(CTX.timeline.format().pixelAspect()), # noqa "openpype.project.useOCIOEnvironmentOverride": project.useOCIOEnvironmentOverride(), # noqa "openpype.project.lutSetting16Bit": project.lutSetting16Bit(), "openpype.project.lutSetting8Bit": project.lutSetting8Bit(), @@ -339,10 +326,10 @@ def _create_otio_timeline(): }) start_time = create_otio_rational_time( - self.timeline.timecodeStart(), self.project_fps) + CTX.timeline.timecodeStart(), CTX.project_fps) return otio.schema.Timeline( - name=self.timeline.name(), + name=CTX.timeline.name(), global_start_time=start_time, metadata=metadata ) @@ -351,7 +338,7 @@ def _create_otio_timeline(): def create_otio_track(track_type, track_name): return otio.schema.Track( name=track_name, - kind=self.track_types[track_type] + kind=CTX.track_types[track_type] ) @@ -363,7 +350,7 @@ def add_otio_gap(track_item, otio_track, prev_out): gap = otio.opentime.TimeRange( duration=otio.opentime.RationalTime( gap_length, - self.project_fps + CTX.project_fps ) ) otio_gap = otio.schema.Gap(source_range=gap) @@ -396,14 +383,14 @@ def create_otio_timeline(): return track_item.parent().items()[itemindex - 1] # get current timeline - self.timeline = hiero.ui.activeSequence() - self.project_fps = self.timeline.framerate().toFloat() + CTX.timeline = hiero.ui.activeSequence() + CTX.project_fps = CTX.timeline.framerate().toFloat() # convert timeline to otio otio_timeline = _create_otio_timeline() # loop all defined track types - for track in self.timeline.items(): + for track in CTX.timeline.items(): # skip if track is disabled if not track.isEnabled(): continue @@ -441,7 +428,7 @@ def create_otio_timeline(): otio_track.append(otio_clip) # Add tags as markers - if self.include_tags: + if CTX.include_tags: create_otio_markers(otio_track, track) # add track to otio timeline From 2e684f107a6d355a82df97b9ede4187733881bb9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 27 Jun 2023 18:04:48 +0200 Subject: [PATCH 22/80] updating hiero api with nicer code - creating constants module for api wide constants - creating CTX class for passing module wide variables instead of using fake self --- openpype/hosts/hiero/api/__init__.py | 13 ++++- openpype/hosts/hiero/api/constants.py | 3 + openpype/hosts/hiero/api/lib.py | 56 +++++++++---------- .../plugins/publish/precollect_instances.py | 2 +- .../plugins/publish/precollect_workfile.py | 3 +- 5 files changed, 44 insertions(+), 33 deletions(-) create mode 100644 openpype/hosts/hiero/api/constants.py diff --git a/openpype/hosts/hiero/api/__init__.py b/openpype/hosts/hiero/api/__init__.py index b95c0fe1d7..099db14794 100644 --- a/openpype/hosts/hiero/api/__init__.py +++ b/openpype/hosts/hiero/api/__init__.py @@ -21,8 +21,13 @@ from .pipeline import ( reset_selection ) +from .constants import ( + OPENPYPE_TAG_NAME, + DEFAULT_SEQUENCE_NAME, + DEFAULT_BIN_NAME +) + from .lib import ( - pype_tag_name, flatten, get_track_items, get_current_project, @@ -82,8 +87,12 @@ __all__ = [ "file_extensions", "work_root", + # Constants + "OPENPYPE_TAG_NAME", + "DEFAULT_SEQUENCE_NAME", + "DEFAULT_BIN_NAME", + # Lib functions - "pype_tag_name", "flatten", "get_track_items", "get_current_project", diff --git a/openpype/hosts/hiero/api/constants.py b/openpype/hosts/hiero/api/constants.py new file mode 100644 index 0000000000..61a780af33 --- /dev/null +++ b/openpype/hosts/hiero/api/constants.py @@ -0,0 +1,3 @@ +OPENPYPE_TAG_NAME = "openpypeData" +DEFAULT_SEQUENCE_NAME = "openpypeSequence" +DEFAULT_BIN_NAME = "openpypeBin" diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py index fa874f9e9d..def79d70f1 100644 --- a/openpype/hosts/hiero/api/lib.py +++ b/openpype/hosts/hiero/api/lib.py @@ -5,7 +5,6 @@ Host specific functions where host api is connected from copy import deepcopy import os import re -import sys import platform import functools import warnings @@ -29,12 +28,22 @@ from openpype.pipeline import ( from openpype.pipeline.load import filter_containers from openpype.lib import Logger from . import tags - +from .constants import ( + OPENPYPE_TAG_NAME, + DEFAULT_SEQUENCE_NAME, + DEFAULT_BIN_NAME +) from openpype.pipeline.colorspace import ( get_imageio_config ) +class CTX: + _has_been_setup = False + _has_menu = False + _parent_gui = None + + class DeprecatedWarning(DeprecationWarning): pass @@ -82,15 +91,6 @@ def deprecated(new_destination): log = Logger.get_logger(__name__) -self = sys.modules[__name__] -self._has_been_setup = False -self._has_menu = False -self._registered_gui = None -self._parent = None -self.pype_tag_name = "openpypeData" -self.default_sequence_name = "openpypeSequence" -self.default_bin_name = "openpypeBin" - def flatten(_list): for item in _list: @@ -131,7 +131,7 @@ def get_current_sequence(name=None, new=False): if new: # create new - name = name or self.default_sequence_name + name = name or DEFAULT_SEQUENCE_NAME sequence = hiero.core.Sequence(name) root_bin.addItem(hiero.core.BinItem(sequence)) elif name: @@ -345,7 +345,7 @@ def get_track_item_tags(track_item): # collect all tags which are not openpype tag returning_tag_data.extend( tag for tag in _tags - if tag.name() != self.pype_tag_name + if tag.name() != OPENPYPE_TAG_NAME ) return returning_tag_data @@ -385,7 +385,7 @@ def set_track_openpype_tag(track, data=None): # if pype tag available then update with input data tag = tags.create_tag( "{}_{}".format( - self.pype_tag_name, + OPENPYPE_TAG_NAME, _get_tag_unique_hash() ), tag_data @@ -412,7 +412,7 @@ def get_track_openpype_tag(track): return None for tag in _tags: # return only correct tag defined by global name - if self.pype_tag_name in tag.name(): + if OPENPYPE_TAG_NAME in tag.name(): return tag @@ -484,7 +484,7 @@ def get_trackitem_openpype_tag(track_item): return None for tag in _tags: # return only correct tag defined by global name - if self.pype_tag_name in tag.name(): + if OPENPYPE_TAG_NAME in tag.name(): return tag @@ -516,7 +516,7 @@ def set_trackitem_openpype_tag(track_item, data=None): # if pype tag available then update with input data tag = tags.create_tag( "{}_{}".format( - self.pype_tag_name, + OPENPYPE_TAG_NAME, _get_tag_unique_hash() ), tag_data @@ -698,29 +698,29 @@ def setup(console=False, port=None, menu=True): menu (bool, optional): Display file menu in Hiero. """ - if self._has_been_setup: + if CTX._has_been_setup: teardown() add_submission() if menu: add_to_filemenu() - self._has_menu = True + CTX._has_menu = True - self._has_been_setup = True + CTX._has_been_setup = True log.debug("pyblish: Loaded successfully.") def teardown(): """Remove integration""" - if not self._has_been_setup: + if not CTX._has_been_setup: return - if self._has_menu: + if CTX._has_menu: remove_from_filemenu() - self._has_menu = False + CTX._has_menu = False - self._has_been_setup = False + CTX._has_been_setup = False log.debug("pyblish: Integration torn down successfully") @@ -928,7 +928,7 @@ def create_bin(path=None, project=None): # get the first loaded project project = project or get_current_project() - path = path or self.default_bin_name + path = path or DEFAULT_BIN_NAME path = path.replace("\\", "/").split("/") @@ -1311,11 +1311,11 @@ def before_project_save(event): def get_main_window(): """Acquire Nuke's main window""" - if self._parent is None: + if CTX._parent_gui is None: top_widgets = QtWidgets.QApplication.topLevelWidgets() name = "Foundry::UI::DockMainWindow" main_window = next(widget for widget in top_widgets if widget.inherits("QMainWindow") and widget.metaObject().className() == name) - self._parent = main_window - return self._parent + CTX._parent_gui = main_window + return CTX._parent_gui diff --git a/openpype/hosts/hiero/plugins/publish/precollect_instances.py b/openpype/hosts/hiero/plugins/publish/precollect_instances.py index bb02919b35..3f9da2cf60 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_instances.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_instances.py @@ -310,7 +310,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin): # add pypedata marker to otio_clip metadata for marker in otio_clip.markers: - if phiero.pype_tag_name in marker.name: + if phiero.OPENPYPE_TAG_NAME in marker.name: otio_clip.metadata.update(marker.metadata) return {"otioClip": otio_clip} diff --git a/openpype/hosts/hiero/plugins/publish/precollect_workfile.py b/openpype/hosts/hiero/plugins/publish/precollect_workfile.py index 08963f98fd..1f477c1639 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_workfile.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_workfile.py @@ -8,7 +8,6 @@ from qtpy.QtGui import QPixmap import hiero.ui from openpype.pipeline import legacy_io -from openpype.hosts.hiero import api as phiero from openpype.hosts.hiero.api.otio import hiero_export @@ -22,8 +21,8 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): asset = legacy_io.Session["AVALON_ASSET"] subset = "workfile" - project = phiero.get_current_project() active_timeline = hiero.ui.activeSequence() + project = active_timeline.project() fps = active_timeline.framerate().toFloat() # adding otio timeline to context From a6fe26e31788267561633e664573f5064e473b91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Wed, 28 Jun 2023 11:42:22 +0200 Subject: [PATCH 23/80] Update openpype/hosts/hiero/api/otio/hiero_export.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/hiero/api/otio/hiero_export.py | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/hiero/api/otio/hiero_export.py b/openpype/hosts/hiero/api/otio/hiero_export.py index e6d6196196..35db477407 100644 --- a/openpype/hosts/hiero/api/otio/hiero_export.py +++ b/openpype/hosts/hiero/api/otio/hiero_export.py @@ -11,20 +11,21 @@ import hiero.core import hiero.ui +TRACK_TYPE_MAP = { + hiero.core.VideoTrack: otio.schema.TrackKind.Video, + hiero.core.AudioTrack: otio.schema.TrackKind.Audio +} +MARKER_COLOR_MAP = { + "magenta": otio.schema.MarkerColor.MAGENTA, + "red": otio.schema.MarkerColor.RED, + "yellow": otio.schema.MarkerColor.YELLOW, + "green": otio.schema.MarkerColor.GREEN, + "cyan": otio.schema.MarkerColor.CYAN, + "blue": otio.schema.MarkerColor.BLUE, +} + class CTX: - track_types = { - hiero.core.VideoTrack: otio.schema.TrackKind.Video, - hiero.core.AudioTrack: otio.schema.TrackKind.Audio - } project_fps = None - marker_color_map = { - "magenta": otio.schema.MarkerColor.MAGENTA, - "red": otio.schema.MarkerColor.RED, - "yellow": otio.schema.MarkerColor.YELLOW, - "green": otio.schema.MarkerColor.GREEN, - "cyan": otio.schema.MarkerColor.CYAN, - "blue": otio.schema.MarkerColor.BLUE, - } timeline = None include_tags = True From cf8c30abcfb4cf801742e4853222dc3cfb7df11d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Wed, 28 Jun 2023 11:42:29 +0200 Subject: [PATCH 24/80] Update openpype/hosts/hiero/api/lib.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/hiero/api/lib.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py index def79d70f1..057af49f64 100644 --- a/openpype/hosts/hiero/api/lib.py +++ b/openpype/hosts/hiero/api/lib.py @@ -38,10 +38,10 @@ from openpype.pipeline.colorspace import ( ) -class CTX: - _has_been_setup = False - _has_menu = False - _parent_gui = None +class _CTX: + has_been_setup = False + has_menu = False + parent_gui = None class DeprecatedWarning(DeprecationWarning): From 04f7c125b0ab4d4849164fdf4a40aa09f1f69f30 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 28 Jun 2023 11:49:36 +0200 Subject: [PATCH 25/80] fixes after suggestions --- openpype/hosts/hiero/api/lib.py | 30 +++++++++---------- openpype/hosts/hiero/api/otio/hiero_export.py | 21 ++++++------- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py index 057af49f64..09d73f5cc2 100644 --- a/openpype/hosts/hiero/api/lib.py +++ b/openpype/hosts/hiero/api/lib.py @@ -92,13 +92,13 @@ def deprecated(new_destination): log = Logger.get_logger(__name__) -def flatten(_list): - for item in _list: - if isinstance(item, (list, tuple)): - for sub_item in flatten(item): +def flatten(list_): + for item_ in list_: + if isinstance(item_, (list, tuple)): + for sub_item in flatten(item_): yield sub_item else: - yield item + yield item_ def get_current_project(remove_untitled=False): @@ -698,29 +698,29 @@ def setup(console=False, port=None, menu=True): menu (bool, optional): Display file menu in Hiero. """ - if CTX._has_been_setup: + if _CTX.has_been_setup: teardown() add_submission() if menu: add_to_filemenu() - CTX._has_menu = True + _CTX.has_menu = True - CTX._has_been_setup = True + _CTX.has_been_setup = True log.debug("pyblish: Loaded successfully.") def teardown(): """Remove integration""" - if not CTX._has_been_setup: + if not _CTX.has_been_setup: return - if CTX._has_menu: + if _CTX.has_menu: remove_from_filemenu() - CTX._has_menu = False + _CTX.has_menu = False - CTX._has_been_setup = False + _CTX.has_been_setup = False log.debug("pyblish: Integration torn down successfully") @@ -1311,11 +1311,11 @@ def before_project_save(event): def get_main_window(): """Acquire Nuke's main window""" - if CTX._parent_gui is None: + if _CTX.parent_gui is None: top_widgets = QtWidgets.QApplication.topLevelWidgets() name = "Foundry::UI::DockMainWindow" main_window = next(widget for widget in top_widgets if widget.inherits("QMainWindow") and widget.metaObject().className() == name) - CTX._parent_gui = main_window - return CTX._parent_gui + _CTX.parent_gui = main_window + return _CTX.parent_gui diff --git a/openpype/hosts/hiero/api/otio/hiero_export.py b/openpype/hosts/hiero/api/otio/hiero_export.py index 35db477407..de547f3046 100644 --- a/openpype/hosts/hiero/api/otio/hiero_export.py +++ b/openpype/hosts/hiero/api/otio/hiero_export.py @@ -3,7 +3,6 @@ import os import re -import sys import ast import opentimelineio as otio from . import utils @@ -23,19 +22,21 @@ MARKER_COLOR_MAP = { "cyan": otio.schema.MarkerColor.CYAN, "blue": otio.schema.MarkerColor.BLUE, } - + + class CTX: project_fps = None timeline = None include_tags = True -def flatten(_list): - for item in _list: - if isinstance(item, (list, tuple)): - for sub_item in flatten(item): + +def flatten(list_): + for item_ in list_: + if isinstance(item_, (list, tuple)): + for sub_item in flatten(item_): yield sub_item else: - yield item + yield item_ def create_otio_rational_time(frame, fps): @@ -205,8 +206,8 @@ def get_marker_color(tag): res = re.search(pat, icon) if res: color = res.groupdict().get('color') - if color.lower() in CTX.marker_color_map: - return CTX.marker_color_map[color.lower()] + if color.lower() in MARKER_COLOR_MAP: + return MARKER_COLOR_MAP[color.lower()] return otio.schema.MarkerColor.RED @@ -339,7 +340,7 @@ def _create_otio_timeline(): def create_otio_track(track_type, track_name): return otio.schema.Track( name=track_name, - kind=CTX.track_types[track_type] + kind=TRACK_TYPE_MAP[track_type] ) From 7a164032e031b8425aacdd2b998bed7617d5ba85 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 28 Jun 2023 18:35:41 +0100 Subject: [PATCH 26/80] Working callback for managing Xgen sidecar files. --- openpype/hosts/maya/api/pipeline.py | 103 ++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index 5323717fa7..27d489418f 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -2,6 +2,7 @@ import os import errno import logging import contextlib +import shutil from maya import utils, cmds, OpenMaya import maya.api.OpenMaya as om @@ -113,6 +114,9 @@ class MayaHost(HostBase, IWorkfileHost, ILoadHost): register_event_callback("taskChanged", on_task_changed) register_event_callback("workfile.open.before", before_workfile_open) register_event_callback("workfile.save.before", before_workfile_save) + register_event_callback( + "workfile.save.before", workfile_save_before_xgen + ) register_event_callback("workfile.save.before", after_workfile_save) def open_workfile(self, filepath): @@ -681,6 +685,105 @@ def before_workfile_save(event): create_workspace_mel(workdir_path, project_name) +def display_warning(message, show_cancel=False): + """Show feedback to user. + + Returns: + bool + """ + + from qtpy import QtWidgets + + accept = QtWidgets.QMessageBox.Ok + if show_cancel: + buttons = accept | QtWidgets.QMessageBox.Cancel + else: + buttons = accept + + state = QtWidgets.QMessageBox.warning( + None, + "", + message, + buttons=buttons, + defaultButton=accept + ) + + return state == accept + + +def workfile_save_before_xgen(event): + current_work_dir = legacy_io.Session["AVALON_WORKDIR"].replace("\\", "/") + expected_work_dir = event.data["workdir_path"].replace("\\", "/") + if current_work_dir == expected_work_dir: + return + + palettes = cmds.ls(type="xgmPalette", long=True) + if not palettes: + return + + import xgenm + + transfers = [] + overwrites = [] + attribute_changes = {} + attrs = ["xgFileName", "xgBaseFile"] + for palette in palettes: + project_path = xgenm.getAttr("xgProjectPath", palette.replace("|", "")) + _, maya_extension = os.path.splitext(event.data["filename"]) + + for attr in attrs: + node_attr = "{}.{}".format(palette, attr) + attr_value = cmds.getAttr(node_attr) + + if not attr_value: + continue + + source = os.path.join(project_path, attr_value) + + attr_value = event.data["filename"].replace( + maya_extension, + "__{}{}".format( + palette.replace("|", "").replace(":", "__"), + os.path.splitext(attr_value)[1] + ) + ) + target = os.path.join(expected_work_dir, attr_value) + + transfers.append((source, target)) + attribute_changes[node_attr] = attr_value + + relative_path = xgenm.getAttr( + "xgDataPath", palette.replace("|", "") + ).split(os.pathsep)[0] + absolute_path = relative_path.replace("${PROJECT}", project_path) + for root, _, files in os.walk(absolute_path): + for f in files: + source = os.path.join(root, f).replace("\\", "/") + target = source.replace(project_path, expected_work_dir + "/") + transfers.append((source, target)) + if os.path.exists(target): + overwrites.append(target) + + # Ask user about overwriting files. + msg = ( + "WARNING! Potential loss of data.\n\n" + "Found duplicate Xgen files in new context.\n" + "Do you want to overwrite?\n\n{}".format("\n".join(overwrites)) + ) + if overwrites: + accept = display_warning(msg, show_cancel=True) + if not accept: + return + + for attribute, value in attribute_changes.items(): + cmds.setAttr(attribute, value, type="string") + + for source, destination in transfers: + if not os.path.exists(os.path.dirname(destination)): + os.makedirs(os.path.dirname(destination)) + shutil.copy(source, destination) + + def after_workfile_save(event): workfile_name = event["filename"] if ( From f947e2efe7cca2b359f4e574a133206cd3a6041c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 29 Jun 2023 00:46:53 +0200 Subject: [PATCH 27/80] Fix version comparison --- openpype/pipeline/colorspace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 269825f85f..d5f2624155 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -240,7 +240,7 @@ def get_data_subprocess(config_path, data_type): def compatible_python(): """Only 3.9 or higher can directly use PyOpenColorIO in ocio_wrapper""" compatible = False - if sys.version[0] == 3 and sys.version[1] >= 9: + if sys.version_info.major == 3 and sys.version_info.minor >= 9: compatible = True return compatible From 25a03628c949df07af76dbedb1b30767f8ffc37f Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 29 Jun 2023 08:49:43 +0100 Subject: [PATCH 28/80] BigRoy feedback --- openpype/hosts/maya/api/pipeline.py | 39 +++++++++++++++++++---------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index 27d489418f..45064d53f9 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -712,6 +712,20 @@ def display_warning(message, show_cancel=False): def workfile_save_before_xgen(event): + """Manage Xgen external files when switching context. + + Xgen has various external files that needs to be unique and relative to the + workfile, so we need to copy and potentially overwrite these files when + switching context. + + Args: + event (Event) - openpype/lib/events.py + """ + if not cmds.pluginInfo("xgenToolkit", query=True, loaded=True): + return + + import xgenm + current_work_dir = legacy_io.Session["AVALON_WORKDIR"].replace("\\", "/") expected_work_dir = event.data["workdir_path"].replace("\\", "/") if current_work_dir == expected_work_dir: @@ -721,14 +735,13 @@ def workfile_save_before_xgen(event): if not palettes: return - import xgenm - transfers = [] overwrites = [] attribute_changes = {} attrs = ["xgFileName", "xgBaseFile"] for palette in palettes: - project_path = xgenm.getAttr("xgProjectPath", palette.replace("|", "")) + sanitized_palette = palette.replace("|", "") + project_path = xgenm.getAttr("xgProjectPath", sanitized_palette) _, maya_extension = os.path.splitext(event.data["filename"]) for attr in attrs: @@ -743,7 +756,7 @@ def workfile_save_before_xgen(event): attr_value = event.data["filename"].replace( maya_extension, "__{}{}".format( - palette.replace("|", "").replace(":", "__"), + sanitized_palette.replace(":", "__"), os.path.splitext(attr_value)[1] ) ) @@ -753,7 +766,7 @@ def workfile_save_before_xgen(event): attribute_changes[node_attr] = attr_value relative_path = xgenm.getAttr( - "xgDataPath", palette.replace("|", "") + "xgDataPath", sanitized_palette ).split(os.pathsep)[0] absolute_path = relative_path.replace("${PROJECT}", project_path) for root, _, files in os.walk(absolute_path): @@ -765,24 +778,24 @@ def workfile_save_before_xgen(event): overwrites.append(target) # Ask user about overwriting files. - msg = ( - "WARNING! Potential loss of data.\n\n" - "Found duplicate Xgen files in new context.\n" - "Do you want to overwrite?\n\n{}".format("\n".join(overwrites)) - ) if overwrites: + msg = ( + "WARNING! Potential loss of data.\n\n" + "Found duplicate Xgen files in new context.\n" + "Do you want to overwrite?\n\n{}".format("\n".join(overwrites)) + ) accept = display_warning(msg, show_cancel=True) if not accept: return - for attribute, value in attribute_changes.items(): - cmds.setAttr(attribute, value, type="string") - for source, destination in transfers: if not os.path.exists(os.path.dirname(destination)): os.makedirs(os.path.dirname(destination)) shutil.copy(source, destination) + for attribute, value in attribute_changes.items(): + cmds.setAttr(attribute, value, type="string") + def after_workfile_save(event): workfile_name = event["filename"] From a93d2b9c996835200ce9e03509e9533655caab85 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 29 Jun 2023 10:13:37 +0200 Subject: [PATCH 29/80] Use Houdini's values directly --- openpype/hosts/houdini/api/lib.py | 23 ++++------------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index a33ba7aad2..a32e9d8d61 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -633,23 +633,8 @@ def evalParmNoFrame(node, parm, pad_character="#"): def get_color_management_preferences(): """Get default OCIO preferences""" - data = { - "config": hou.Color.ocio_configPath() - + return { + "config": hou.Color.ocio_configPath(), + "display": hou.Color.ocio_defaultDisplay(), + "view": hou.Color.ocio_defaultView() } - - # Get default display and view from OCIO - display = hou.Color.ocio_defaultDisplay() - disp_regex = re.compile(r"^(?P.+-)(?P.+)$") - disp_match = disp_regex.match(display) - - view = hou.Color.ocio_defaultView() - view_regex = re.compile(r"^(?P.+- )(?P.+)$") - view_match = view_regex.match(view) - data.update({ - "display": disp_match.group("display"), - "view": view_match.group("view") - - }) - - return data From 7ca847ce247d7278190143885d4cb5a3a6ba5267 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 29 Jun 2023 10:36:41 +0100 Subject: [PATCH 30/80] Change name of warning method. --- openpype/hosts/maya/api/pipeline.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index 8e5c2f9fb0..98ebd9f028 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -685,7 +685,7 @@ def before_workfile_save(event): create_workspace_mel(workdir_path, project_name) -def display_warning(message, show_cancel=False): +def prompt_warning(message, show_cancel=False): """Show feedback to user. Returns: @@ -784,7 +784,7 @@ def workfile_save_before_xgen(event): "Found duplicate Xgen files in new context.\n" "Do you want to overwrite?\n\n{}".format("\n".join(overwrites)) ) - accept = display_warning(msg, show_cancel=True) + accept = prompt_warning(msg, show_cancel=True) if not accept: return From 00ab5c2be0d21fbca9f3468df571b75e96be6d59 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 29 Jun 2023 19:19:11 +0300 Subject: [PATCH 31/80] add parm value for RS_outputFileFormat --- .../hosts/houdini/plugins/create/create_redshift_rop.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_redshift_rop.py b/openpype/hosts/houdini/plugins/create/create_redshift_rop.py index e14ff15bf8..c7d874c492 100644 --- a/openpype/hosts/houdini/plugins/create/create_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_redshift_rop.py @@ -3,12 +3,12 @@ import hou # noqa from openpype.hosts.houdini.api import plugin -from openpype.pipeline import CreatedInstance from openpype.lib import EnumDef class CreateRedshiftROP(plugin.HoudiniCreator): """Redshift ROP""" + identifier = "io.openpype.creators.houdini.redshift_rop" label = "Redshift ROP" family = "redshift_rop" @@ -57,6 +57,8 @@ class CreateRedshiftROP(plugin.HoudiniCreator): fmt="${aov}.$F4.{ext}".format(aov="AOV", ext=ext) ) + ext_format_index = {"exr": 0, "tif": 1, "jpg": 2, "png": 3} + parms = { # Render frame range "trange": 1, @@ -64,6 +66,7 @@ class CreateRedshiftROP(plugin.HoudiniCreator): "RS_outputFileNamePrefix": filepath, "RS_outputMultilayerMode": "1", # no multi-layered exr "RS_outputBeautyAOVSuffix": "beauty", + "RS_outputFileFormat": ext_format_index[ext], } if self.selected_nodes: @@ -93,8 +96,7 @@ class CreateRedshiftROP(plugin.HoudiniCreator): def get_pre_create_attr_defs(self): attrs = super(CreateRedshiftROP, self).get_pre_create_attr_defs() image_format_enum = [ - "bmp", "cin", "exr", "jpg", "pic", "pic.gz", "png", - "rad", "rat", "rta", "sgi", "tga", "tif", + "exr", "tif", "jpg", "png", ] return attrs + [ From 55c646df82e7ddcfcbbf8309bb69ddcd8ad4bc96 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 30 Jun 2023 08:33:44 +0300 Subject: [PATCH 32/80] remove the unnecessary comment --- openpype/hosts/houdini/plugins/create/create_redshift_rop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/create/create_redshift_rop.py b/openpype/hosts/houdini/plugins/create/create_redshift_rop.py index c7d874c492..4576e9a721 100644 --- a/openpype/hosts/houdini/plugins/create/create_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_redshift_rop.py @@ -28,7 +28,7 @@ class CreateRedshiftROP(plugin.HoudiniCreator): instance = super(CreateRedshiftROP, self).create( subset_name, instance_data, - pre_create_data) # type: CreatedInstance + pre_create_data) instance_node = hou.node(instance.get("instance_node")) From aa5236128b5a8f63cfa0e2495c28ad1adaf61693 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Fri, 30 Jun 2023 08:15:07 +0100 Subject: [PATCH 33/80] Update openpype/hosts/maya/api/pipeline.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/maya/api/pipeline.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index 98ebd9f028..c2e4ffaba0 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -688,9 +688,9 @@ def before_workfile_save(event): def prompt_warning(message, show_cancel=False): """Show feedback to user. - Returns: - bool - """ + Returns: + bool + """ from qtpy import QtWidgets From 2a40984858d20d47a7f99f5b8ca070d0e63159f4 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 30 Jun 2023 08:16:37 +0100 Subject: [PATCH 34/80] import at top --- openpype/hosts/maya/api/pipeline.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index 98ebd9f028..33ab2ac71e 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -8,6 +8,7 @@ from maya import utils, cmds, OpenMaya import maya.api.OpenMaya as om import pyblish.api +from qtpy import QtWidgets from openpype.settings import get_project_settings from openpype.host import ( @@ -691,9 +692,6 @@ def prompt_warning(message, show_cancel=False): Returns: bool """ - - from qtpy import QtWidgets - accept = QtWidgets.QMessageBox.Ok if show_cancel: buttons = accept | QtWidgets.QMessageBox.Cancel From 0033efe5b181f78e4fe13216305933ecaa1fb447 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Fri, 30 Jun 2023 15:08:38 +0200 Subject: [PATCH 35/80] catch assertion error --- openpype/hosts/maya/api/workfile_template_builder.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/workfile_template_builder.py b/openpype/hosts/maya/api/workfile_template_builder.py index e2f30f46d0..ba3435b60d 100644 --- a/openpype/hosts/maya/api/workfile_template_builder.py +++ b/openpype/hosts/maya/api/workfile_template_builder.py @@ -272,7 +272,12 @@ class MayaPlaceholderLoadPlugin(PlaceholderPlugin, PlaceholderLoadMixin): return roots = cmds.sets(container, q=True) - ref_node = get_reference_node(roots) + ref_node = None + try: + ref_node = get_reference_node(roots) + except AssertionError as e: + self.log.info(e.args[0]) + nodes_to_parent = [] for root in roots: if ref_node: From 13820bf99b54dd42e702da8df027497e9932b1f0 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Fri, 30 Jun 2023 15:15:06 +0200 Subject: [PATCH 36/80] indent correction --- openpype/hosts/maya/api/workfile_template_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/workfile_template_builder.py b/openpype/hosts/maya/api/workfile_template_builder.py index ba3435b60d..865f497710 100644 --- a/openpype/hosts/maya/api/workfile_template_builder.py +++ b/openpype/hosts/maya/api/workfile_template_builder.py @@ -274,7 +274,7 @@ class MayaPlaceholderLoadPlugin(PlaceholderPlugin, PlaceholderLoadMixin): roots = cmds.sets(container, q=True) ref_node = None try: - ref_node = get_reference_node(roots) + ref_node = get_reference_node(roots) except AssertionError as e: self.log.info(e.args[0]) From 3e93d163c03048b801b654dcd0400cbecf2bd7f4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 30 Jun 2023 16:11:37 +0200 Subject: [PATCH 37/80] check PyOpenColorIO rather then python version https://github.com/ynput/OpenPype/pull/5212#issuecomment-1614651292 --- openpype/pipeline/colorspace.py | 36 +++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index d5f2624155..244463ad41 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -1,7 +1,6 @@ from copy import deepcopy import re import os -import sys import json import platform import contextlib @@ -237,12 +236,13 @@ def get_data_subprocess(config_path, data_type): return json.loads(return_json_data) -def compatible_python(): - """Only 3.9 or higher can directly use PyOpenColorIO in ocio_wrapper""" - compatible = False - if sys.version_info.major == 3 and sys.version_info.minor >= 9: - compatible = True - return compatible +def compatibility_check(): + """Making sure PyOpenColorIO is importable""" + try: + import PyOpenColorIO + except (ImportError, ModuleNotFoundError): + return False + return True def get_ocio_config_colorspaces(config_path): @@ -257,12 +257,15 @@ def get_ocio_config_colorspaces(config_path): Returns: dict: colorspace and family in couple """ - if compatible_python(): - from ..scripts.ocio_wrapper import _get_colorspace_data - return _get_colorspace_data(config_path) - else: + if not compatibility_check(): + # python environment is not compatible with PyOpenColorIO + # needs to be run in subprocess return get_colorspace_data_subprocess(config_path) + from openpype.scripts.ocio_wrapper import _get_colorspace_data + + return _get_colorspace_data(config_path) + def get_colorspace_data_subprocess(config_path): """Get colorspace data via subprocess @@ -290,12 +293,15 @@ def get_ocio_config_views(config_path): Returns: dict: `display/viewer` and viewer data """ - if compatible_python(): - from ..scripts.ocio_wrapper import _get_views_data - return _get_views_data(config_path) - else: + if not compatibility_check(): + # python environment is not compatible with PyOpenColorIO + # needs to be run in subprocess return get_views_data_subprocess(config_path) + from openpype.scripts.ocio_wrapper import _get_views_data + + return _get_views_data(config_path) + def get_views_data_subprocess(config_path): """Get viewers data via subprocess From 051882bb3e0bf59c265c96718b2c7fa264230191 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 30 Jun 2023 16:16:12 +0200 Subject: [PATCH 38/80] noqa excepetion --- openpype/pipeline/colorspace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 244463ad41..bda14d275d 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -239,7 +239,7 @@ def get_data_subprocess(config_path, data_type): def compatibility_check(): """Making sure PyOpenColorIO is importable""" try: - import PyOpenColorIO + import PyOpenColorIO # noqa: F401 except (ImportError, ModuleNotFoundError): return False return True From 131f8ddd8998372782b80ce73ec936f6254f1358 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Fri, 30 Jun 2023 16:23:11 +0200 Subject: [PATCH 39/80] Update openpype/pipeline/colorspace.py Co-authored-by: Roy Nieterau --- openpype/pipeline/colorspace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index bda14d275d..3f2d4891c1 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -240,7 +240,7 @@ def compatibility_check(): """Making sure PyOpenColorIO is importable""" try: import PyOpenColorIO # noqa: F401 - except (ImportError, ModuleNotFoundError): + except ImportError: return False return True From f1e64a063eab0080f9a1b2160b382581374eb126 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 30 Jun 2023 16:37:57 +0200 Subject: [PATCH 40/80] Maya: Convert frame values to integers (#5188) * convert frame values to integers * convert instance frame information to integer * fix condition in collect instances * Convert frame handles to int. * Make sure handles are integers Co-authored-by: Toke Jepsen * convert the float frame range value getting from attributes in maya to integer * hound shut * move int conversion later * safe context value conversion --------- Co-authored-by: Toke Stuart Jepsen Co-authored-by: Kayla Man --- .../maya/plugins/publish/collect_instances.py | 33 ++++++++++++++----- .../publish/collect_maya_scene_time.py | 14 ++++---- .../maya/plugins/publish/extract_playblast.py | 4 +-- 3 files changed, 34 insertions(+), 17 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_instances.py b/openpype/hosts/maya/plugins/publish/collect_instances.py index 87a4de162d..74bdc11a2c 100644 --- a/openpype/hosts/maya/plugins/publish/collect_instances.py +++ b/openpype/hosts/maya/plugins/publish/collect_instances.py @@ -104,25 +104,40 @@ class CollectInstances(pyblish.api.ContextPlugin): # Define nice label name = cmds.ls(objset, long=False)[0] # use short name - label = "{0} ({1})".format(name, - data["asset"]) + label = "{0} ({1})".format(name, data["asset"]) + + # Convert frame values to integers + for attr_name in ( + "handleStart", "handleEnd", "frameStart", "frameEnd", + ): + value = data.get(attr_name) + if value is not None: + data[attr_name] = int(value) # Append start frame and end frame to label if present - if "frameStart" and "frameEnd" in data: + if "frameStart" in data and "frameEnd" in data: # Take handles from context if not set locally on the instance for key in ["handleStart", "handleEnd"]: if key not in data: - data[key] = context.data[key] + value = context.data[key] + if value is not None: + value = int(value) + data[key] = value - data["frameStartHandle"] = data["frameStart"] - data["handleStart"] # noqa: E501 - data["frameEndHandle"] = data["frameEnd"] + data["handleEnd"] # noqa: E501 + data["frameStartHandle"] = int( + data["frameStart"] - data["handleStart"] + ) + data["frameEndHandle"] = int( + data["frameEnd"] + data["handleEnd"] + ) - label += " [{0}-{1}]".format(int(data["frameStartHandle"]), - int(data["frameEndHandle"])) + label += " [{0}-{1}]".format( + data["frameStartHandle"], data["frameEndHandle"] + ) instance.data["label"] = label - instance.data.update(data) + self.log.debug("{}".format(instance.data)) # Produce diagnostic message for any graphical # user interface interested in visualising it. diff --git a/openpype/hosts/maya/plugins/publish/collect_maya_scene_time.py b/openpype/hosts/maya/plugins/publish/collect_maya_scene_time.py index 7e198df14d..6a20cb151f 100644 --- a/openpype/hosts/maya/plugins/publish/collect_maya_scene_time.py +++ b/openpype/hosts/maya/plugins/publish/collect_maya_scene_time.py @@ -17,10 +17,12 @@ class CollectMayaSceneTime(pyblish.api.InstancePlugin): def process(self, instance): instance.data.update({ - "frameStart": cmds.playbackOptions(query=True, minTime=True), - "frameEnd": cmds.playbackOptions(query=True, maxTime=True), - "frameStartHandle": cmds.playbackOptions(query=True, - animationStartTime=True), - "frameEndHandle": cmds.playbackOptions(query=True, - animationEndTime=True) + "frameStart": int( + cmds.playbackOptions(query=True, minTime=True)), + "frameEnd": int( + cmds.playbackOptions(query=True, maxTime=True)), + "frameStartHandle": int( + cmds.playbackOptions(query=True, animationStartTime=True)), + "frameEndHandle": int( + cmds.playbackOptions(query=True, animationEndTime=True)) }) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 3ceef6f3d3..9580c13841 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -261,8 +261,8 @@ class ExtractPlayblast(publish.Extractor): "ext": capture_preset["Codec"]["compression"], "files": collected_files, "stagingDir": stagingdir, - "frameStart": start, - "frameEnd": end, + "frameStart": int(start), + "frameEnd": int(end), "fps": fps, "tags": tags, "camera_name": camera_node_name From 8e9f4eb40e4d5595c9786f4b6dc7ffd9a4dcfdc6 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 30 Jun 2023 15:40:31 +0100 Subject: [PATCH 41/80] Log message about overwrites and continue --- openpype/hosts/maya/api/pipeline.py | 35 +++++------------------------ 1 file changed, 5 insertions(+), 30 deletions(-) diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index 8cc4359205..9fab825105 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -686,30 +686,6 @@ def before_workfile_save(event): create_workspace_mel(workdir_path, project_name) -def prompt_warning(message, show_cancel=False): - """Show feedback to user. - - Returns: - bool - """ - - accept = QtWidgets.QMessageBox.Ok - if show_cancel: - buttons = accept | QtWidgets.QMessageBox.Cancel - else: - buttons = accept - - state = QtWidgets.QMessageBox.warning( - None, - "", - message, - buttons=buttons, - defaultButton=accept - ) - - return state == accept - - def workfile_save_before_xgen(event): """Manage Xgen external files when switching context. @@ -778,14 +754,13 @@ def workfile_save_before_xgen(event): # Ask user about overwriting files. if overwrites: - msg = ( + log.warning( "WARNING! Potential loss of data.\n\n" - "Found duplicate Xgen files in new context.\n" - "Do you want to overwrite?\n\n{}".format("\n".join(overwrites)) + "Found duplicate Xgen files in new context.\n{}".format( + "\n".join(overwrites) + ) ) - accept = prompt_warning(msg, show_cancel=True) - if not accept: - return + return for source, destination in transfers: if not os.path.exists(os.path.dirname(destination)): From e7bdf25b9a126bf0e4cffbcc6a2529db1f15af37 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 30 Jun 2023 15:41:39 +0100 Subject: [PATCH 42/80] HOund --- openpype/hosts/maya/api/pipeline.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index 9fab825105..b4042fd3d7 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -8,7 +8,6 @@ from maya import utils, cmds, OpenMaya import maya.api.OpenMaya as om import pyblish.api -from qtpy import QtWidgets from openpype.settings import get_project_settings from openpype.host import ( From 8713759c18ba8e5b617ada2add27bbc154cc6069 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 30 Jun 2023 15:53:08 +0100 Subject: [PATCH 43/80] Hound --- openpype/hosts/maya/api/pipeline.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index b4042fd3d7..9fab825105 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -8,6 +8,7 @@ from maya import utils, cmds, OpenMaya import maya.api.OpenMaya as om import pyblish.api +from qtpy import QtWidgets from openpype.settings import get_project_settings from openpype.host import ( From aaf3a9acfefa4f7f9663c4d96ec273e09a7d4f0c Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 30 Jun 2023 15:53:34 +0100 Subject: [PATCH 44/80] Hound --- openpype/hosts/maya/api/pipeline.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index 9fab825105..b4042fd3d7 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -8,7 +8,6 @@ from maya import utils, cmds, OpenMaya import maya.api.OpenMaya as om import pyblish.api -from qtpy import QtWidgets from openpype.settings import get_project_settings from openpype.host import ( From a2ef4e00138a9e5613516fcba3c10dc45e45f206 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 30 Jun 2023 22:05:45 +0200 Subject: [PATCH 45/80] Restructure `set_colorspace` - Only set `configFilePath` when OCIO env var is not set since it doesn't do anything if OCIO var is set anyway. - Set the Maya 2022+ default OCIO path using the resources path instead of "" to avoid Maya Save File on new file after launch (this also fixes the Save prompt on open last workfile feature with Global color management enabled) - Move all code related to applying the maya settings together after querying the settings - Swap around the `if use_workfile_settings` since the check was reversed - Use `get_current_project_name()` instead of environment vars --- openpype/hosts/maya/api/lib.py | 92 ++++++++++++++++++---------------- 1 file changed, 50 insertions(+), 42 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index ce851d2dbe..f7ddab9f1e 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -3238,36 +3238,21 @@ def iter_shader_edits(relationships, shader_nodes, nodes_by_id, label=None): def set_colorspace(): - """Set Colorspace from project configuration - """ + """Set Colorspace from project configuration""" - # set color spaces for rendering space and view transforms - def _colormanage(**kwargs): - """Wrapper around `cmds.colorManagementPrefs`. - - This logs errors instead of raising an error so color management - settings get applied as much as possible. - - """ - assert len(kwargs) == 1, "Must receive one keyword argument" - try: - cmds.colorManagementPrefs(edit=True, **kwargs) - log.debug("Setting Color Management Preference: {}".format(kwargs)) - except RuntimeError as exc: - log.error(exc) - - project_name = os.getenv("AVALON_PROJECT") + project_name = get_current_project_name() imageio = get_project_settings(project_name)["maya"]["imageio"] # ocio compatibility variables ocio_v2_maya_version = 2022 maya_version = int(cmds.about(version=True)) ocio_v2_support = use_ocio_v2 = maya_version >= ocio_v2_maya_version + is_ocio_set = bool(os.environ.get("OCIO")) - root_dict = {} use_workfile_settings = imageio.get("workfile", {}).get("enabled") - if use_workfile_settings: + root_dict = imageio["workfile"] + else: # TODO: deprecated code from 3.15.5 - remove # Maya 2022+ introduces new OCIO v2 color management settings that # can override the old color management preferences. OpenPype has @@ -3290,40 +3275,63 @@ def set_colorspace(): if not isinstance(root_dict, dict): msg = "set_colorspace(): argument should be dictionary" log.error(msg) + return - else: - root_dict = imageio["workfile"] + # backward compatibility + # TODO: deprecated code from 3.15.5 - remove with deprecated code above + view_name = root_dict.get("viewTransform") + if view_name is None: + view_name = root_dict.get("viewName") log.debug(">> root_dict: {}".format(pformat(root_dict))) + if not root_dict: + return - if root_dict: - # enable color management - cmds.colorManagementPrefs(e=True, cmEnabled=True) - cmds.colorManagementPrefs(e=True, ocioRulesEnabled=True) + # set color spaces for rendering space and view transforms + def _colormanage(**kwargs): + """Wrapper around `cmds.colorManagementPrefs`. - # backward compatibility - # TODO: deprecated code from 3.15.5 - refactor to use new settings - view_name = root_dict.get("viewTransform") - if view_name is None: - view_name = root_dict.get("viewName") + This logs errors instead of raising an error so color management + settings get applied as much as possible. - if use_ocio_v2: - # Use Maya 2022+ default OCIO v2 config + """ + assert len(kwargs) == 1, "Must receive one keyword argument" + try: + cmds.colorManagementPrefs(edit=True, **kwargs) + log.debug("Setting Color Management Preference: {}".format(kwargs)) + except RuntimeError as exc: + log.error(exc) + + # enable color management + cmds.colorManagementPrefs(edit=True, cmEnabled=True) + cmds.colorManagementPrefs(edit=True, ocioRulesEnabled=True) + + if use_ocio_v2: + log.info("Using Maya OCIO v2") + if not is_ocio_set: + # Set the Maya 2022+ default OCIO v2 config file path log.info("Setting default Maya OCIO v2 config") - cmds.colorManagementPrefs(edit=True, configFilePath="") + # Note: Setting "" as value also sets this default however + # introduces a bug where launching a file on startup will prompt + # to save the empty scene before it, so we set using the path. + # This value has been the same for 2022, 2023 and 2024 + path = "/OCIO-configs/Maya2022-default/config.ocio" + cmds.colorManagementPrefs(edit=True, configFilePath=path) - # set rendering space and view transform - _colormanage(renderingSpaceName=root_dict["renderSpace"]) - _colormanage(viewName=view_name) - _colormanage(displayName=root_dict["displayName"]) - else: + # set rendering space and view transform + _colormanage(renderingSpaceName=root_dict["renderSpace"]) + _colormanage(viewName=view_name) + _colormanage(displayName=root_dict["displayName"]) + else: + log.info("Using Maya OCIO v1 (legacy)") + if not is_ocio_set: # Set the Maya default config file path log.info("Setting default Maya OCIO v1 legacy config") cmds.colorManagementPrefs(edit=True, configFilePath="legacy") - # set rendering space and view transform - _colormanage(renderingSpaceName=root_dict["renderSpace"]) - _colormanage(viewTransformName=view_name) + # set rendering space and view transform + _colormanage(renderingSpaceName=root_dict["renderSpace"]) + _colormanage(viewTransformName=view_name) @contextlib.contextmanager From 0b6f9dc8f2e872becb2d058a593dc0d1e02842e0 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 30 Jun 2023 22:53:21 +0200 Subject: [PATCH 46/80] Apply deferred renderlayer observer changes all at once (1x deferred) and use `maya.utils.executeDeferred` --- openpype/hosts/maya/api/pipeline.py | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index 4ab915cc7a..ba7b37e1e0 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -563,21 +563,20 @@ def on_save(): lib.set_id(node, new_id, overwrite=False) +def _update_render_layer_observers(): + # Helper to trigger update for all renderlayer observer logic + lib.remove_render_layer_observer() + lib.add_render_layer_observer() + lib.add_render_layer_change_observer() + + def on_open(): """On scene open let's assume the containers have changed.""" from qtpy import QtWidgets from openpype.widgets import popup - cmds.evalDeferred( - "from openpype.hosts.maya.api import lib;" - "lib.remove_render_layer_observer()") - cmds.evalDeferred( - "from openpype.hosts.maya.api import lib;" - "lib.add_render_layer_observer()") - cmds.evalDeferred( - "from openpype.hosts.maya.api import lib;" - "lib.add_render_layer_change_observer()") + utils.executeDeferred(_update_render_layer_observers) # # Update current task for the current scene # update_task_from_path(cmds.file(query=True, sceneName=True)) @@ -618,16 +617,9 @@ def on_new(): """Set project resolution and fps when create a new file""" log.info("Running callback on new..") with lib.suspended_refresh(): - cmds.evalDeferred( - "from openpype.hosts.maya.api import lib;" - "lib.remove_render_layer_observer()") - cmds.evalDeferred( - "from openpype.hosts.maya.api import lib;" - "lib.add_render_layer_observer()") - cmds.evalDeferred( - "from openpype.hosts.maya.api import lib;" - "lib.add_render_layer_change_observer()") lib.set_context_settings() + + utils.executeDeferred(_update_render_layer_observers) _remove_workfile_lock() From bc15989b2e310ba41016d99d621c74191ce50624 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 30 Jun 2023 22:54:26 +0200 Subject: [PATCH 47/80] Cleanup: Remove old commented code --- openpype/hosts/maya/api/pipeline.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index ba7b37e1e0..96c2f2d0af 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -549,14 +549,10 @@ def on_save(): Any transform of a mesh, without an existing ID, is given one automatically on file save. """ - log.info("Running callback on save..") # remove lockfile if users jumps over from one scene to another _remove_workfile_lock() - # # Update current task for the current scene - # update_task_from_path(cmds.file(query=True, sceneName=True)) - # Generate ids of the current context on nodes in the scene nodes = lib.get_id_required_nodes(referenced_nodes=False) for node, new_id in lib.generate_ids(nodes): @@ -577,8 +573,6 @@ def on_open(): from openpype.widgets import popup utils.executeDeferred(_update_render_layer_observers) - # # Update current task for the current scene - # update_task_from_path(cmds.file(query=True, sceneName=True)) # Validate FPS after update_task_from_path to # ensure it is using correct FPS for the asset From 667e8373b91784873d3ffc21bb9b5db59279b1f7 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 30 Jun 2023 22:55:47 +0200 Subject: [PATCH 48/80] Cleanup `fix_incompatile_containers` - Do not just print all loader names always :) - Define invalid names ones - Use a set for faster lookups - Log some decent info message whenever it does trigger on legacy scene --- openpype/hosts/maya/api/lib.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index ce851d2dbe..ad02faaf58 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -2811,19 +2811,22 @@ def get_attr_in_layer(attr, layer): def fix_incompatible_containers(): """Backwards compatibility: old containers to use new ReferenceLoader""" - + old_loaders = { + "MayaAsciiLoader", + "AbcLoader", + "ModelLoader", + "CameraLoader", + "RigLoader", + "FBXLoader" + } host = registered_host() for container in host.ls(): loader = container['loader'] - - print(container['loader']) - - if loader in ["MayaAsciiLoader", - "AbcLoader", - "ModelLoader", - "CameraLoader", - "RigLoader", - "FBXLoader"]: + if loader in old_loaders: + log.info( + "Converting legacy container loader {} to " + "ReferenceLoader: {}".format(loader, container["objectName"]) + ) cmds.setAttr(container["objectName"] + ".loader", "ReferenceLoader", type="string") From 843cbb2387eff6db7bee6aafb972a69f3855ed7a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 30 Jun 2023 22:56:13 +0200 Subject: [PATCH 49/80] Minor optimization to look only for exact type matches --- openpype/hosts/maya/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index ad02faaf58..25d59ba093 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -2954,7 +2954,7 @@ def _get_render_instances(): list: list of instances """ - objectset = cmds.ls("*.id", long=True, type="objectSet", + objectset = cmds.ls("*.id", long=True, exactType="objectSet", recursive=True, objectsOnly=True) instances = [] From 6c07372f78df50314b6908b569b98b10a4fb05b8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 30 Jun 2023 23:01:29 +0200 Subject: [PATCH 50/80] Use `get_main_window` logic from `lib` --- openpype/hosts/maya/api/pipeline.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index 96c2f2d0af..33b4f8bc65 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -569,7 +569,6 @@ def _update_render_layer_observers(): def on_open(): """On scene open let's assume the containers have changed.""" - from qtpy import QtWidgets from openpype.widgets import popup utils.executeDeferred(_update_render_layer_observers) @@ -583,10 +582,7 @@ def on_open(): log.warning("Scene has outdated content.") # Find maya main window - top_level_widgets = {w.objectName(): w for w in - QtWidgets.QApplication.topLevelWidgets()} - parent = top_level_widgets.get("MayaWindow", None) - + parent = lib.get_main_window() if parent is None: log.info("Skipping outdated content pop-up " "because Maya window can't be found.") From fcde6ecb296b8d44e253c173b723e96faee056b9 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 30 Jun 2023 23:05:50 +0200 Subject: [PATCH 51/80] Only apply UI related tweaks when not in headless mode --- openpype/hosts/maya/api/pipeline.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index 33b4f8bc65..6bdb6d6034 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -480,18 +480,16 @@ def on_init(): # Force load objExport plug-in (requested by artists) cmds.loadPlugin("objExport", quiet=True) - from .customize import ( - override_component_mask_commands, - override_toolbox_ui - ) - safe_deferred(override_component_mask_commands) - - launch_workfiles = os.environ.get("WORKFILES_STARTUP") - - if launch_workfiles: - safe_deferred(host_tools.show_workfiles) - if not lib.IS_HEADLESS: + launch_workfiles = os.environ.get("WORKFILES_STARTUP") + if launch_workfiles: + safe_deferred(host_tools.show_workfiles) + + from .customize import ( + override_component_mask_commands, + override_toolbox_ui + ) + safe_deferred(override_component_mask_commands) safe_deferred(override_toolbox_ui) From 7a79c58da0a87267fff416dca1b92a3fa3d5ae16 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 30 Jun 2023 23:17:51 +0200 Subject: [PATCH 52/80] Re-use `lib.get_main_window` logic --- openpype/hosts/maya/tools/mayalookassigner/app.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/tools/mayalookassigner/app.py b/openpype/hosts/maya/tools/mayalookassigner/app.py index 13da999c2d..64fc04dfc4 100644 --- a/openpype/hosts/maya/tools/mayalookassigner/app.py +++ b/openpype/hosts/maya/tools/mayalookassigner/app.py @@ -8,7 +8,10 @@ from openpype.client import get_last_version_by_subset_id from openpype import style from openpype.pipeline import legacy_io from openpype.tools.utils.lib import qt_app_context -from openpype.hosts.maya.api.lib import assign_look_by_version +from openpype.hosts.maya.api.lib import ( + assign_look_by_version, + get_main_window +) from maya import cmds # old api for MFileIO @@ -297,9 +300,7 @@ def show(): pass # Get Maya main window - top_level_widgets = QtWidgets.QApplication.topLevelWidgets() - mainwindow = next(widget for widget in top_level_widgets - if widget.objectName() == "MayaWindow") + mainwindow = get_main_window() with qt_app_context(): window = MayaLookAssignerWindow(parent=mainwindow) From ad161379df5261ca10dff56f2c98687ee7e4ae7b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 30 Jun 2023 23:18:25 +0200 Subject: [PATCH 53/80] Remove unused import --- openpype/hosts/maya/api/lib.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 25d59ba093..cccd3d1672 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -3,7 +3,6 @@ import os from pprint import pformat import sys -import platform import uuid import re From 9fc5b5fd0035e432c95897785307c91066c15024 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 30 Jun 2023 23:20:41 +0200 Subject: [PATCH 54/80] Remove unused empty file --- openpype/hosts/maya/api/obj.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 openpype/hosts/maya/api/obj.py diff --git a/openpype/hosts/maya/api/obj.py b/openpype/hosts/maya/api/obj.py deleted file mode 100644 index e69de29bb2..0000000000 From 951b9590332e97461487f7171d27ee3757aaaae7 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 30 Jun 2023 23:20:55 +0200 Subject: [PATCH 55/80] Remove redundant comment --- openpype/hosts/maya/plugins/publish/extract_obj.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_obj.py b/openpype/hosts/maya/plugins/publish/extract_obj.py index edfe0b9439..518b0f0ff8 100644 --- a/openpype/hosts/maya/plugins/publish/extract_obj.py +++ b/openpype/hosts/maya/plugins/publish/extract_obj.py @@ -2,7 +2,6 @@ import os from maya import cmds -# import maya.mel as mel import pyblish.api from openpype.pipeline import publish from openpype.hosts.maya.api import lib From c14255da22cad6a958a14c0e2bdcc3769990e98a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 30 Jun 2023 23:21:33 +0200 Subject: [PATCH 56/80] Remove unused import --- openpype/hosts/maya/api/plugin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index 967d39674c..0971251469 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -1,5 +1,4 @@ import os -import re from maya import cmds From 3c8e8bab543a557187a40efa0b2a03795a562619 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 30 Jun 2023 23:26:02 +0200 Subject: [PATCH 57/80] Debug log instead of print spamming on opening (legacy) Creator UI --- openpype/pipeline/create/legacy_create.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/pipeline/create/legacy_create.py b/openpype/pipeline/create/legacy_create.py index 7380e9f9c7..50ef274633 100644 --- a/openpype/pipeline/create/legacy_create.py +++ b/openpype/pipeline/create/legacy_create.py @@ -74,12 +74,12 @@ class LegacyCreator(object): if not plugin_settings: return - print(">>> We have preset for {}".format(plugin_name)) + cls.log.debug(">>> We have preset for {}".format(plugin_name)) for option, value in plugin_settings.items(): if option == "enabled" and value is False: - print(" - is disabled by preset") + cls.log.debug(" - is disabled by preset") else: - print(" - setting `{}`: `{}`".format(option, value)) + cls.log.debug(" - setting `{}`: `{}`".format(option, value)) setattr(cls, option, value) def process(self): From d88735bc1ed44a279904932fbbc1a2c8584db6eb Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 30 Jun 2023 23:27:20 +0200 Subject: [PATCH 58/80] Remove unused comment --- openpype/hosts/maya/api/render_setup_tools.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/maya/api/render_setup_tools.py b/openpype/hosts/maya/api/render_setup_tools.py index 2ad59810d0..a6b46e1e9a 100644 --- a/openpype/hosts/maya/api/render_setup_tools.py +++ b/openpype/hosts/maya/api/render_setup_tools.py @@ -15,7 +15,6 @@ import contextlib from maya import cmds from maya.app.renderSetup.model import renderSetup -# from colorbleed.maya import lib from .lib import pairwise From 344d456214720b6f73c0a693646aa9b39cf0cfd2 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 1 Jul 2023 03:33:14 +0000 Subject: [PATCH 59/80] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index c19af05373..bc3cb93882 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.12-nightly.1" +__version__ = "3.15.12-nightly.2" From d792ca37e3e1f53d48facab858774d7b406df8de Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 1 Jul 2023 03:34:07 +0000 Subject: [PATCH 60/80] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 5d464d0532..652dbb8597 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.15.12-nightly.2 - 3.15.12-nightly.1 - 3.15.11 - 3.15.11-nightly.5 @@ -134,7 +135,6 @@ body: - 3.14.4-nightly.4 - 3.14.4-nightly.3 - 3.14.4-nightly.2 - - 3.14.4-nightly.1 validations: required: true - type: dropdown From 76b6fed6a7fec021be19f0ae104f1499781b8f5e Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov Date: Mon, 3 Jul 2023 03:41:37 +0300 Subject: [PATCH 61/80] disable delivery button if no representations checked fix macos combobox layout add error message if no delivery templates found --- openpype/plugins/load/delivery.py | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/openpype/plugins/load/delivery.py b/openpype/plugins/load/delivery.py index d1d5659118..9509cf3b8c 100644 --- a/openpype/plugins/load/delivery.py +++ b/openpype/plugins/load/delivery.py @@ -1,4 +1,5 @@ import copy +import platform from collections import defaultdict from qtpy import QtWidgets, QtCore, QtGui @@ -83,6 +84,12 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): self.templates = self._get_templates(self.anatomy) for name, _ in self.templates.items(): dropdown.addItem(name) + if self.templates and platform.system() == "Darwin": + # fix macos QCombobox Style + dropdown.setItemDelegate(QtWidgets.QStyledItemDelegate()) + # update combo box length to longest entry + 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)) @@ -115,7 +122,7 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): input_layout.addRow("Representations", repre_checkboxes_layout) btn_delivery = QtWidgets.QPushButton("Deliver") - btn_delivery.setEnabled(bool(dropdown.currentText())) + btn_delivery.setEnabled(False) progress_bar = QtWidgets.QProgressBar(self) progress_bar.setMinimum = 0 @@ -148,9 +155,17 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): self._update_selected_label() self._update_template_value() - - btn_delivery.clicked.connect(self.deliver) - dropdown.currentIndexChanged.connect(self._update_template_value) + if not self.dropdown.count(): + self.text_area.setVisible(True) + error_message = ( + "No Delivery Templates found!\n" + "Add Template in [project_anatomy/templates/delivery]" + ) + self.text_area.setText(error_message) + self.log.error(error_message.replace("\n", " ")) + else: + btn_delivery.clicked.connect(self.deliver) + dropdown.currentIndexChanged.connect(self._update_template_value) def deliver(self): """Main method to loop through all selected representations""" @@ -287,14 +302,17 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): self.files_selected, self.size_selected = \ self._get_counts(selected_repres) self.selected_label.setText(self._prepare_label()) + # update delivery button state if any templates found + if self.dropdown.count(): + self.btn_delivery.setEnabled(bool(selected_repres)) def _update_template_value(self, _index=None): """Sets template value to label after selection in dropdown.""" name = self.dropdown.currentText() template_value = self.templates.get(name) if template_value: - self.btn_delivery.setEnabled(True) self.template_label.setText(template_value) + self.btn_delivery.setEnabled(bool(self._get_selected_repres())) def _update_progress(self, uploaded): """Update progress bar after each repre copied.""" From 5f8a07aa8fb6c5931165e6a60772d8a1d5537665 Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov Date: Mon, 3 Jul 2023 12:30:43 +0300 Subject: [PATCH 62/80] remove unnecessary else statement --- openpype/plugins/load/delivery.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/plugins/load/delivery.py b/openpype/plugins/load/delivery.py index 9509cf3b8c..90e734973b 100644 --- a/openpype/plugins/load/delivery.py +++ b/openpype/plugins/load/delivery.py @@ -155,6 +155,10 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): self._update_selected_label() self._update_template_value() + + btn_delivery.clicked.connect(self.deliver) + dropdown.currentIndexChanged.connect(self._update_template_value) + if not self.dropdown.count(): self.text_area.setVisible(True) error_message = ( @@ -163,9 +167,6 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): ) self.text_area.setText(error_message) self.log.error(error_message.replace("\n", " ")) - else: - btn_delivery.clicked.connect(self.deliver) - dropdown.currentIndexChanged.connect(self._update_template_value) def deliver(self): """Main method to loop through all selected representations""" From fe58cb3c7cc4520b61c8b73c166f6694c5d29e91 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 3 Jul 2023 13:42:00 +0200 Subject: [PATCH 63/80] Enhancement: More descriptive error messages for Loaders (#5227) * More descriptive error messages for Loaders * Restructure error reporting --- openpype/pipeline/load/__init__.py | 4 ++ openpype/pipeline/load/utils.py | 37 +++++++++++++++---- .../tools/sceneinventory/switch_dialog.py | 24 +++++++++--- 3 files changed, 52 insertions(+), 13 deletions(-) diff --git a/openpype/pipeline/load/__init__.py b/openpype/pipeline/load/__init__.py index e9ac0df924..7320a9f0e8 100644 --- a/openpype/pipeline/load/__init__.py +++ b/openpype/pipeline/load/__init__.py @@ -4,6 +4,8 @@ from .utils import ( LoadError, IncompatibleLoaderError, InvalidRepresentationContext, + LoaderSwitchNotImplementedError, + LoaderNotFoundError, get_repres_contexts, get_contexts_for_repre_docs, @@ -55,6 +57,8 @@ __all__ = ( "LoadError", "IncompatibleLoaderError", "InvalidRepresentationContext", + "LoaderSwitchNotImplementedError", + "LoaderNotFoundError", "get_repres_contexts", "get_contexts_for_repre_docs", diff --git a/openpype/pipeline/load/utils.py b/openpype/pipeline/load/utils.py index fefdb8537b..2c40280ccd 100644 --- a/openpype/pipeline/load/utils.py +++ b/openpype/pipeline/load/utils.py @@ -79,6 +79,16 @@ class InvalidRepresentationContext(ValueError): pass +class LoaderSwitchNotImplementedError(NotImplementedError): + """Error when `switch` is used with Loader that has no implementation.""" + pass + + +class LoaderNotFoundError(RuntimeError): + """Error when Loader plugin for a loader name is not found.""" + pass + + def get_repres_contexts(representation_ids, dbcon=None): """Return parenthood context for representation. @@ -432,7 +442,10 @@ def remove_container(container): Loader = _get_container_loader(container) if not Loader: - raise RuntimeError("Can't remove container. See log for details.") + raise LoaderNotFoundError( + "Can't remove container because loader '{}' was not found." + .format(container.get("loader")) + ) loader = Loader(get_representation_context(container["representation"])) return loader.remove(container) @@ -480,7 +493,10 @@ def update_container(container, version=-1): # Run update on the Loader for this container Loader = _get_container_loader(container) if not Loader: - raise RuntimeError("Can't update container. See log for details.") + raise LoaderNotFoundError( + "Can't update container because loader '{}' was not found." + .format(container.get("loader")) + ) loader = Loader(get_representation_context(container["representation"])) return loader.update(container, new_representation) @@ -502,15 +518,18 @@ def switch_container(container, representation, loader_plugin=None): loader_plugin = _get_container_loader(container) if not loader_plugin: - raise RuntimeError("Can't switch container. See log for details.") + raise LoaderNotFoundError( + "Can't switch container because loader '{}' was not found." + .format(container.get("loader")) + ) if not hasattr(loader_plugin, "switch"): # Backwards compatibility (classes without switch support # might be better to just have "switch" raise NotImplementedError # on the base class of Loader\ - raise RuntimeError("Loader '{}' does not support 'switch'".format( - loader_plugin.label - )) + raise LoaderSwitchNotImplementedError( + "Loader {} does not support 'switch'".format(loader_plugin.label) + ) # Get the new representation to switch to project_name = legacy_io.active_project() @@ -520,7 +539,11 @@ def switch_container(container, representation, loader_plugin=None): new_context = get_representation_context(new_representation) if not is_compatible_loader(loader_plugin, new_context): - raise AssertionError("Must be compatible Loader") + raise IncompatibleLoaderError( + "Loader {} is incompatible with {}".format( + loader_plugin.__name__, new_context["subset"]["name"] + ) + ) loader = loader_plugin(new_context) diff --git a/openpype/tools/sceneinventory/switch_dialog.py b/openpype/tools/sceneinventory/switch_dialog.py index 4aaad38bbc..ce2272df57 100644 --- a/openpype/tools/sceneinventory/switch_dialog.py +++ b/openpype/tools/sceneinventory/switch_dialog.py @@ -19,6 +19,9 @@ from openpype.pipeline.load import ( switch_container, get_repres_contexts, loaders_from_repre_context, + LoaderSwitchNotImplementedError, + IncompatibleLoaderError, + LoaderNotFoundError ) from .widgets import ( @@ -1298,19 +1301,28 @@ class SwitchAssetDialog(QtWidgets.QDialog): else: repre_doc = repres_by_name[container_repre_name] + error = None try: switch_container(container, repre_doc, loader) + except ( + LoaderSwitchNotImplementedError, + IncompatibleLoaderError, + LoaderNotFoundError, + ) as exc: + error = str(exc) except Exception: - msg = ( + error = ( + "Switch asset failed. " + "Search console log for more details." + ) + if error is not None: + log.warning(( "Couldn't switch asset." "See traceback for more information." - ) - log.warning(msg, exc_info=True) + ), exc_info=True) dialog = QtWidgets.QMessageBox(self) dialog.setWindowTitle("Switch asset failed") - dialog.setText( - "Switch asset failed. Search console log for more details" - ) + dialog.setText(error) dialog.exec_() self.switched.emit() From 09f0d183d8915efe72ad00056eee4955ced4cd09 Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov Date: Mon, 3 Jul 2023 15:40:16 +0300 Subject: [PATCH 64/80] hound remove whitespace --- openpype/plugins/load/delivery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/load/delivery.py b/openpype/plugins/load/delivery.py index 90e734973b..4bd4f6e9cf 100644 --- a/openpype/plugins/load/delivery.py +++ b/openpype/plugins/load/delivery.py @@ -155,7 +155,7 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): self._update_selected_label() self._update_template_value() - + btn_delivery.clicked.connect(self.deliver) dropdown.currentIndexChanged.connect(self._update_template_value) From f2315c6fd812308a605a9aba21d00473a8bb8c79 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Mon, 3 Jul 2023 16:40:35 +0300 Subject: [PATCH 65/80] add geometry check --- .../plugins/publish/validate_abc_primitive_to_detail.py | 8 ++++++++ .../plugins/publish/validate_primitive_hierarchy_paths.py | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/openpype/hosts/houdini/plugins/publish/validate_abc_primitive_to_detail.py b/openpype/hosts/houdini/plugins/publish/validate_abc_primitive_to_detail.py index 86e92a052f..bef8db45a4 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_abc_primitive_to_detail.py +++ b/openpype/hosts/houdini/plugins/publish/validate_abc_primitive_to_detail.py @@ -73,6 +73,14 @@ class ValidateAbcPrimitiveToDetail(pyblish.api.InstancePlugin): cls.log.debug("Checking Primitive to Detail pattern: %s" % pattern) cls.log.debug("Checking with path attribute: %s" % path_attr) + if not hasattr(output_node, "geometry"): + # In the case someone has explicitly set an Object + # node instead of a SOP node in Geometry context + # then for now we ignore - this allows us to also + # export object transforms. + cls.log.warning("No geometry output node found, skipping check..") + return + # Check if the primitive attribute exists frame = instance.data.get("frameStart", 0) geo = output_node.geometryAtFrame(frame) diff --git a/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py b/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py index d3a4c0cfbf..cd5e724ab3 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py +++ b/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py @@ -60,6 +60,14 @@ class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin): cls.log.debug("Checking for attribute: %s" % path_attr) + if not hasattr(output_node, "geometry"): + # In the case someone has explicitly set an Object + # node instead of a SOP node in Geometry context + # then for now we ignore - this allows us to also + # export object transforms. + cls.log.warning("No geometry output node found, skipping check..") + return + # Check if the primitive attribute exists frame = instance.data.get("frameStart", 0) geo = output_node.geometryAtFrame(frame) From 744095eb161939dec966f6e52c215b72c775a521 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 3 Jul 2023 16:39:40 +0200 Subject: [PATCH 66/80] maya: allign default settings to distributed aces 1.2 config --- openpype/settings/defaults/project_settings/maya.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 19c3da13e6..e3fc5f0723 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -421,9 +421,9 @@ }, "workfile": { "enabled": false, - "renderSpace": "ACEScg", - "displayName": "sRGB", - "viewName": "ACES 1.0 SDR-video" + "renderSpace": "ACES - ACEScg", + "displayName": "ACES", + "viewName": "sRGB" }, "colorManagementPreference_v2": { "enabled": true, From 39385995785f353db4c1d9521760cae46c637035 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Mon, 3 Jul 2023 18:07:34 +0300 Subject: [PATCH 67/80] add geometry check --- .../hosts/houdini/plugins/publish/validate_vdb_output_node.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py index 674782179c..ee46b746a2 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py +++ b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py @@ -55,6 +55,8 @@ def get_geometry_at_frame(sop_node, frame, force=True): """Return geometry at frame but force a cooked value.""" with update_mode_context(hou.updateMode.AutoUpdate): sop_node.cook(force=force, frame_range=(frame, frame)) + if not hasattr(sop_node, "geometry"): + return return sop_node.geometryAtFrame(frame) From 9ccbaed576bb93b076c8d05664cb2a6bf3e289b9 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 3 Jul 2023 23:12:59 +0800 Subject: [PATCH 68/80] include only one setting in collect_textures, only in publish tab --- .../substancepainter/plugins/create/create_textures.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/openpype/hosts/substancepainter/plugins/create/create_textures.py b/openpype/hosts/substancepainter/plugins/create/create_textures.py index dece4b2cc1..1907f0e549 100644 --- a/openpype/hosts/substancepainter/plugins/create/create_textures.py +++ b/openpype/hosts/substancepainter/plugins/create/create_textures.py @@ -34,7 +34,6 @@ class CreateTextures(Creator): if not substance_painter.project.is_open(): raise CreatorError("Can't create a Texture Set instance without " "an open project.") - instance = self.create_instance_in_context(subset_name, instance_data) set_instance( @@ -76,7 +75,6 @@ class CreateTextures(Creator): return instance def get_instance_attr_defs(self): - return [ EnumDef("exportPresetUrl", items=get_export_presets(), @@ -156,7 +154,3 @@ class CreateTextures(Creator): UILabelDef("*only used with " "'Dilation + ' padding"), ] - - def get_pre_create_attr_defs(self): - # Use same attributes as for instance attributes - return self.get_instance_attr_defs() From b6d4f1b142bdc73aeb6d6f402b5897d06b7a31f7 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Tue, 4 Jul 2023 10:53:28 +0300 Subject: [PATCH 69/80] move check outside with --- .../hosts/houdini/plugins/publish/validate_vdb_output_node.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py index ee46b746a2..b51e1007f0 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py +++ b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py @@ -53,10 +53,10 @@ def update_mode_context(mode): def get_geometry_at_frame(sop_node, frame, force=True): """Return geometry at frame but force a cooked value.""" + if not hasattr(sop_node, "geometry"): + return with update_mode_context(hou.updateMode.AutoUpdate): sop_node.cook(force=force, frame_range=(frame, frame)) - if not hasattr(sop_node, "geometry"): - return return sop_node.geometryAtFrame(frame) From 2362c4114e227604974920216997bfc35fe4cfd4 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 4 Jul 2023 17:44:58 +0800 Subject: [PATCH 70/80] align the setting in the create tab with that in the publish tab --- .../plugins/create/create_textures.py | 39 +++++++++++++++++-- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/substancepainter/plugins/create/create_textures.py b/openpype/hosts/substancepainter/plugins/create/create_textures.py index 1907f0e549..9cfa01cee0 100644 --- a/openpype/hosts/substancepainter/plugins/create/create_textures.py +++ b/openpype/hosts/substancepainter/plugins/create/create_textures.py @@ -28,12 +28,37 @@ class CreateTextures(Creator): icon = "picture-o" default_variant = "Main" + image_format = None + exportPresetUrl = None + exportSize = None + exportPadding = "infinite" + exportDilationDistance = 16 def create(self, subset_name, instance_data, pre_create_data): if not substance_painter.project.is_open(): raise CreatorError("Can't create a Texture Set instance without " "an open project.") + self.exportPresetUrl = pre_create_data.get("exportPresetUrl", + self.exportPresetUrl) + instance_data["exportPresetUrl"] = self.exportPresetUrl + + self.image_format = pre_create_data.get("exportFileFormat", + self.image_format) + instance_data["exportFileFormat"] = self.image_format + + self.exportSize = pre_create_data.get("exportSize", + self.exportSize) + instance_data["exportSize"] = self.exportSize + + self.exportPadding = pre_create_data.get("exportPadding", + self.exportPadding) + instance_data["exportPadding"] = self.exportPadding + + self.exportDilationDistance = pre_create_data.get("exportDilationDistance", + self.exportDilationDistance) + instance_data["exportDilationDistance"] = self.exportDilationDistance + instance = self.create_instance_in_context(subset_name, instance_data) set_instance( @@ -75,9 +100,11 @@ class CreateTextures(Creator): return instance def get_instance_attr_defs(self): + return [ EnumDef("exportPresetUrl", items=get_export_presets(), + default=self.exportPresetUrl, label="Output Template"), BoolDef("allowSkippedMaps", label="Allow Skipped Output Maps", @@ -118,7 +145,7 @@ class CreateTextures(Creator): # "psd": "psd", # "sbsar": "sbsar", }, - default=None, + default=self.image_format, label="File type"), EnumDef("exportSize", items={ @@ -132,7 +159,7 @@ class CreateTextures(Creator): 11: "2048", 12: "4096" }, - default=None, + default=self.exportSize, label="Size"), EnumDef("exportPadding", @@ -143,14 +170,18 @@ class CreateTextures(Creator): "color": "Dilation + default background color", "diffusion": "Dilation + diffusion" }, - default="infinite", + default=self.exportPadding, label="Padding"), NumberDef("exportDilationDistance", minimum=0, maximum=256, decimals=0, - default=16, + default=self.exportDilationDistance, label="Dilation Distance"), UILabelDef("*only used with " "'Dilation + ' padding"), ] + + def get_pre_create_attr_defs(self): + # Use same attributes as for instance attributes + return self.get_instance_attr_defs() From 8befb04439dd4cd0a815b88aef13005a4d76b8f9 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 4 Jul 2023 18:31:41 +0800 Subject: [PATCH 71/80] roy's comment --- .../plugins/create/create_textures.py | 43 ++++++------------- .../publish/collect_textureset_images.py | 14 +++--- 2 files changed, 20 insertions(+), 37 deletions(-) diff --git a/openpype/hosts/substancepainter/plugins/create/create_textures.py b/openpype/hosts/substancepainter/plugins/create/create_textures.py index 9cfa01cee0..a835e241b7 100644 --- a/openpype/hosts/substancepainter/plugins/create/create_textures.py +++ b/openpype/hosts/substancepainter/plugins/create/create_textures.py @@ -28,36 +28,22 @@ class CreateTextures(Creator): icon = "picture-o" default_variant = "Main" - image_format = None - exportPresetUrl = None - exportSize = None - exportPadding = "infinite" - exportDilationDistance = 16 def create(self, subset_name, instance_data, pre_create_data): if not substance_painter.project.is_open(): raise CreatorError("Can't create a Texture Set instance without " "an open project.") - self.exportPresetUrl = pre_create_data.get("exportPresetUrl", - self.exportPresetUrl) - instance_data["exportPresetUrl"] = self.exportPresetUrl - - self.image_format = pre_create_data.get("exportFileFormat", - self.image_format) - instance_data["exportFileFormat"] = self.image_format - - self.exportSize = pre_create_data.get("exportSize", - self.exportSize) - instance_data["exportSize"] = self.exportSize - - self.exportPadding = pre_create_data.get("exportPadding", - self.exportPadding) - instance_data["exportPadding"] = self.exportPadding - - self.exportDilationDistance = pre_create_data.get("exportDilationDistance", - self.exportDilationDistance) - instance_data["exportDilationDistance"] = self.exportDilationDistance + # Transfer settings from pre create to instance + for key in [ + "exportPresetUrl", + "exportFileFormat", + "exportSize", + "exportPadding", + "exportDilationDistance" + ]: + if key in pre_create_data: + instance_data[key] = pre_create_data[key] instance = self.create_instance_in_context(subset_name, instance_data) @@ -104,7 +90,6 @@ class CreateTextures(Creator): return [ EnumDef("exportPresetUrl", items=get_export_presets(), - default=self.exportPresetUrl, label="Output Template"), BoolDef("allowSkippedMaps", label="Allow Skipped Output Maps", @@ -145,7 +130,7 @@ class CreateTextures(Creator): # "psd": "psd", # "sbsar": "sbsar", }, - default=self.image_format, + default=None, label="File type"), EnumDef("exportSize", items={ @@ -159,7 +144,7 @@ class CreateTextures(Creator): 11: "2048", 12: "4096" }, - default=self.exportSize, + default=None, label="Size"), EnumDef("exportPadding", @@ -170,13 +155,13 @@ class CreateTextures(Creator): "color": "Dilation + default background color", "diffusion": "Dilation + diffusion" }, - default=self.exportPadding, + default="infinite", label="Padding"), NumberDef("exportDilationDistance", minimum=0, maximum=256, decimals=0, - default=self.exportDilationDistance, + default=16, label="Dilation Distance"), UILabelDef("*only used with " "'Dilation + ' padding"), diff --git a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py index d11abd1019..eb504fafe9 100644 --- a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py +++ b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py @@ -114,7 +114,7 @@ class CollectTextureSet(pyblish.api.InstancePlugin): # Clone the instance image_instance = context.create_instance(image_subset) image_instance[:] = instance[:] - image_instance.data.update(copy.deepcopy(instance.data)) + image_instance.data.update(copy.deepcopy(dict(instance.data))) image_instance.data["name"] = image_subset image_instance.data["label"] = image_subset image_instance.data["subset"] = image_subset @@ -157,9 +157,7 @@ class CollectTextureSet(pyblish.api.InstancePlugin): dict: Export config """ - - creator_attrs = instance.data["creator_attributes"] - preset_url = creator_attrs["exportPresetUrl"] + preset_url = instance.data["exportPresetUrl"] self.log.debug(f"Exporting using preset: {preset_url}") # See: https://substance3d.adobe.com/documentation/ptpy/api/substance_painter/export # noqa @@ -172,10 +170,10 @@ class CollectTextureSet(pyblish.api.InstancePlugin): "exportParameters": [ { "parameters": { - "fileFormat": creator_attrs["exportFileFormat"], - "sizeLog2": creator_attrs["exportSize"], - "paddingAlgorithm": creator_attrs["exportPadding"], - "dilationDistance": creator_attrs["exportDilationDistance"] # noqa + "fileFormat": instance.data["exportFileFormat"], + "sizeLog2": instance.data["exportSize"], + "paddingAlgorithm": instance.data["exportPadding"], + "dilationDistance": instance.data["exportDilationDistance"] # noqa } } ] From 950c5f6b04f34123ed5fd06252bfc539779baa3d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 4 Jul 2023 18:44:26 +0800 Subject: [PATCH 72/80] roy's comment --- .../plugins/create/create_textures.py | 3 ++- .../plugins/publish/collect_textureset_images.py | 14 ++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/substancepainter/plugins/create/create_textures.py b/openpype/hosts/substancepainter/plugins/create/create_textures.py index a835e241b7..d295daf73a 100644 --- a/openpype/hosts/substancepainter/plugins/create/create_textures.py +++ b/openpype/hosts/substancepainter/plugins/create/create_textures.py @@ -35,6 +35,7 @@ class CreateTextures(Creator): raise CreatorError("Can't create a Texture Set instance without " "an open project.") # Transfer settings from pre create to instance + creator_attributes = instance_data.setdefault("creator_attributes", dict()) for key in [ "exportPresetUrl", "exportFileFormat", @@ -43,7 +44,7 @@ class CreateTextures(Creator): "exportDilationDistance" ]: if key in pre_create_data: - instance_data[key] = pre_create_data[key] + creator_attributes[key] = pre_create_data[key] instance = self.create_instance_in_context(subset_name, instance_data) diff --git a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py index eb504fafe9..d11abd1019 100644 --- a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py +++ b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py @@ -114,7 +114,7 @@ class CollectTextureSet(pyblish.api.InstancePlugin): # Clone the instance image_instance = context.create_instance(image_subset) image_instance[:] = instance[:] - image_instance.data.update(copy.deepcopy(dict(instance.data))) + image_instance.data.update(copy.deepcopy(instance.data)) image_instance.data["name"] = image_subset image_instance.data["label"] = image_subset image_instance.data["subset"] = image_subset @@ -157,7 +157,9 @@ class CollectTextureSet(pyblish.api.InstancePlugin): dict: Export config """ - preset_url = instance.data["exportPresetUrl"] + + creator_attrs = instance.data["creator_attributes"] + preset_url = creator_attrs["exportPresetUrl"] self.log.debug(f"Exporting using preset: {preset_url}") # See: https://substance3d.adobe.com/documentation/ptpy/api/substance_painter/export # noqa @@ -170,10 +172,10 @@ class CollectTextureSet(pyblish.api.InstancePlugin): "exportParameters": [ { "parameters": { - "fileFormat": instance.data["exportFileFormat"], - "sizeLog2": instance.data["exportSize"], - "paddingAlgorithm": instance.data["exportPadding"], - "dilationDistance": instance.data["exportDilationDistance"] # noqa + "fileFormat": creator_attrs["exportFileFormat"], + "sizeLog2": creator_attrs["exportSize"], + "paddingAlgorithm": creator_attrs["exportPadding"], + "dilationDistance": creator_attrs["exportDilationDistance"] # noqa } } ] From 21b81ec4e9924346c3e17582a7dabc8d2a2f2d8f Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 4 Jul 2023 18:45:32 +0800 Subject: [PATCH 73/80] hound fix --- .../hosts/substancepainter/plugins/create/create_textures.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/substancepainter/plugins/create/create_textures.py b/openpype/hosts/substancepainter/plugins/create/create_textures.py index d295daf73a..6972ba2794 100644 --- a/openpype/hosts/substancepainter/plugins/create/create_textures.py +++ b/openpype/hosts/substancepainter/plugins/create/create_textures.py @@ -35,7 +35,8 @@ class CreateTextures(Creator): raise CreatorError("Can't create a Texture Set instance without " "an open project.") # Transfer settings from pre create to instance - creator_attributes = instance_data.setdefault("creator_attributes", dict()) + creator_attributes = instance_data.setdefault( + "creator_attributes", dict()) for key in [ "exportPresetUrl", "exportFileFormat", From cf8f7aa9ea8af4e1d6118f19e90012624bdf930a Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 5 Jul 2023 03:31:41 +0000 Subject: [PATCH 74/80] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index bc3cb93882..4a6131a26a 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.12-nightly.2" +__version__ = "3.15.12-nightly.3" From ae05aba0891e7107335b66309ce825eeae8af675 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 5 Jul 2023 03:32:31 +0000 Subject: [PATCH 75/80] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 652dbb8597..9fcb69e2e9 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.15.12-nightly.3 - 3.15.12-nightly.2 - 3.15.12-nightly.1 - 3.15.11 @@ -134,7 +135,6 @@ body: - 3.14.4 - 3.14.4-nightly.4 - 3.14.4-nightly.3 - - 3.14.4-nightly.2 validations: required: true - type: dropdown From cc7a1c0e72c20d1de61f8416246ca6b85ffe404b Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 5 Jul 2023 09:21:12 +0100 Subject: [PATCH 76/80] OPENPYPE_VERSION should only be added when running from build --- openpype/modules/deadline/plugins/publish/submit_publish_job.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 69e9fb6449..292fe58cca 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -146,7 +146,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, "FTRACK_SERVER", "AVALON_APP_NAME", "OPENPYPE_USERNAME", - "OPENPYPE_VERSION", "OPENPYPE_SG_USER" ] From 0d8a42588a0a49c29418e30ecba1aa0165d81c39 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 5 Jul 2023 11:52:32 +0200 Subject: [PATCH 77/80] Fix no prompt for "unsaved changes" showing when opening workfile in Houdini --- openpype/hosts/houdini/api/pipeline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/api/pipeline.py b/openpype/hosts/houdini/api/pipeline.py index b8b8fefb52..8a26bbb504 100644 --- a/openpype/hosts/houdini/api/pipeline.py +++ b/openpype/hosts/houdini/api/pipeline.py @@ -93,7 +93,7 @@ class HoudiniHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): import hdefereval # noqa, hdefereval is only available in ui mode hdefereval.executeDeferred(creator_node_shelves.install) - def has_unsaved_changes(self): + def workfile_has_unsaved_changes(self): return hou.hipFile.hasUnsavedChanges() def get_workfile_extensions(self): From e64328b346b5e8505d668afd56d7d80d5c6511ee Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 5 Jul 2023 11:54:51 +0200 Subject: [PATCH 78/80] CreatePlugin: Get next version helper (#5242) * prepared helper functions to find latest and next versions for instances * added helper method to creator * added new functions to create api * typo fixes * added missing condition * fix dosctring * better cascade of logic --- openpype/pipeline/create/__init__.py | 8 ++ openpype/pipeline/create/context.py | 4 +- openpype/pipeline/create/creator_plugins.py | 23 +++- openpype/pipeline/create/utils.py | 122 ++++++++++++++++++++ 4 files changed, 154 insertions(+), 3 deletions(-) create mode 100644 openpype/pipeline/create/utils.py diff --git a/openpype/pipeline/create/__init__.py b/openpype/pipeline/create/__init__.py index c89fb04c42..5eee18df0f 100644 --- a/openpype/pipeline/create/__init__.py +++ b/openpype/pipeline/create/__init__.py @@ -4,6 +4,11 @@ from .constants import ( PRE_CREATE_THUMBNAIL_KEY, ) +from .utils import ( + get_last_versions_for_instances, + get_next_versions_for_instances, +) + from .subset_name import ( TaskNotSetError, get_subset_name_template, @@ -46,6 +51,9 @@ __all__ = ( "DEFAULT_SUBSET_TEMPLATE", "PRE_CREATE_THUMBNAIL_KEY", + "get_last_versions_for_instances", + "get_next_versions_for_instances", + "TaskNotSetError", "get_subset_name_template", "get_subset_name", diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 332e271b0d..98fcee5fe5 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1122,10 +1122,10 @@ class CreatedInstance: @property def creator_attribute_defs(self): - """Attribute defintions defined by creator plugin. + """Attribute definitions defined by creator plugin. Returns: - List[AbstractAttrDef]: Attribute defitions. + List[AbstractAttrDef]: Attribute definitions. """ return self.creator_attributes.attr_defs diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index 9e47e9cc12..fbb459ab12 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -1,4 +1,3 @@ -import os import copy import collections @@ -21,6 +20,7 @@ from openpype.pipeline.plugin_discover import ( ) from .subset_name import get_subset_name +from .utils import get_next_versions_for_instances from .legacy_create import LegacyCreator @@ -483,6 +483,27 @@ class BaseCreator: thumbnail_path ) + def get_next_versions_for_instances(self, instances): + """Prepare next versions for instances. + + This is helper method to receive next possible versions for instances. + It is using context information on instance to receive them, 'asset' + and 'subset'. + + Output will contain version by each instance id. + + Args: + instances (list[CreatedInstance]): Instances for which to get next + versions. + + Returns: + Dict[str, int]: Next versions by instance id. + """ + + return get_next_versions_for_instances( + self.create_context.project_name, instances + ) + class Creator(BaseCreator): """Creator that has more information for artist to show in UI. diff --git a/openpype/pipeline/create/utils.py b/openpype/pipeline/create/utils.py new file mode 100644 index 0000000000..2ef1f02bd6 --- /dev/null +++ b/openpype/pipeline/create/utils.py @@ -0,0 +1,122 @@ +import collections + +from openpype.client import get_assets, get_subsets, get_last_versions + + +def get_last_versions_for_instances( + project_name, instances, use_value_for_missing=False +): + """Get last versions for instances by their asset and subset name. + + Args: + project_name (str): Project name. + instances (list[CreatedInstance]): Instances to get next versions for. + use_value_for_missing (Optional[bool]): Missing values are replaced + with negative value if True. Otherwise None is used. -2 is used + for instances without filled asset or subset name. -1 is used + for missing entities. + + Returns: + dict[str, Union[int, None]]: Last versions by instance id. + """ + + output = { + instance.id: -1 if use_value_for_missing else None + for instance in instances + } + subset_names_by_asset_name = collections.defaultdict(set) + instances_by_hierarchy = {} + for instance in instances: + asset_name = instance.data.get("asset") + subset_name = instance.subset_name + if not asset_name or not subset_name: + if use_value_for_missing: + output[instance.id] = -2 + continue + + ( + instances_by_hierarchy + .setdefault(asset_name, {}) + .setdefault(subset_name, []) + .append(instance) + ) + subset_names_by_asset_name[asset_name].add(subset_name) + + subset_names = set() + for names in subset_names_by_asset_name.values(): + subset_names |= names + + if not subset_names: + return output + + asset_docs = get_assets( + project_name, + asset_names=subset_names_by_asset_name.keys(), + fields=["name", "_id"] + ) + asset_names_by_id = { + asset_doc["_id"]: asset_doc["name"] + for asset_doc in asset_docs + } + if not asset_names_by_id: + return output + + subset_docs = get_subsets( + project_name, + asset_ids=asset_names_by_id.keys(), + subset_names=subset_names, + fields=["_id", "name", "parent"] + ) + subset_docs_by_id = {} + for subset_doc in subset_docs: + # Filter subset docs by subset names under parent + asset_id = subset_doc["parent"] + asset_name = asset_names_by_id[asset_id] + subset_name = subset_doc["name"] + if subset_name not in subset_names_by_asset_name[asset_name]: + continue + subset_docs_by_id[subset_doc["_id"]] = subset_doc + + if not subset_docs_by_id: + return output + + last_versions_by_subset_id = get_last_versions( + project_name, + subset_docs_by_id.keys(), + fields=["name", "parent"] + ) + for subset_id, version_doc in last_versions_by_subset_id.items(): + subset_doc = subset_docs_by_id[subset_id] + asset_id = subset_doc["parent"] + asset_name = asset_names_by_id[asset_id] + _instances = instances_by_hierarchy[asset_name][subset_doc["name"]] + for instance in _instances: + output[instance.id] = version_doc["name"] + + return output + + +def get_next_versions_for_instances(project_name, instances): + """Get next versions for instances by their asset and subset name. + + Args: + project_name (str): Project name. + instances (list[CreatedInstance]): Instances to get next versions for. + + Returns: + dict[str, Union[int, None]]: Next versions by instance id. Version is + 'None' if instance has no asset or subset name. + """ + + last_versions = get_last_versions_for_instances( + project_name, instances, True) + + output = {} + for instance_id, version in last_versions.items(): + if version == -2: + output[instance_id] = None + elif version == -1: + output[instance_id] = 1 + else: + output[instance_id] = version + 1 + return output From 3016680678aadfce57d01d7e6299187bbfcde70d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 5 Jul 2023 12:00:23 +0200 Subject: [PATCH 79/80] Refactor `has_unsaved_changes` -> `workfile_has_unsaved_changes` in Houdini SaveCurrentScene plugin --- openpype/hosts/houdini/plugins/publish/save_scene.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/publish/save_scene.py b/openpype/hosts/houdini/plugins/publish/save_scene.py index 703d3e4895..3ae3fa3220 100644 --- a/openpype/hosts/houdini/plugins/publish/save_scene.py +++ b/openpype/hosts/houdini/plugins/publish/save_scene.py @@ -19,7 +19,7 @@ class SaveCurrentScene(pyblish.api.ContextPlugin): "Collected filename from current scene name." ) - if host.has_unsaved_changes(): + if host.workfile_has_unsaved_changes(): self.log.info("Saving current file: {}".format(current_file)) host.save_workfile(current_file) else: From bc87b34f66d29aebfaba3a48039b5ab18bab1a60 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 5 Jul 2023 12:02:36 +0200 Subject: [PATCH 80/80] Fix unsaved changes save prompt on open file with workfiles in Substance Painter --- openpype/hosts/substancepainter/api/pipeline.py | 2 +- .../hosts/substancepainter/plugins/publish/save_workfile.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/substancepainter/api/pipeline.py b/openpype/hosts/substancepainter/api/pipeline.py index 9406fb8edb..e96064b2bf 100644 --- a/openpype/hosts/substancepainter/api/pipeline.py +++ b/openpype/hosts/substancepainter/api/pipeline.py @@ -86,7 +86,7 @@ class SubstanceHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): self._uninstall_menu() self._deregister_callbacks() - def has_unsaved_changes(self): + def workfile_has_unsaved_changes(self): if not substance_painter.project.is_open(): return False diff --git a/openpype/hosts/substancepainter/plugins/publish/save_workfile.py b/openpype/hosts/substancepainter/plugins/publish/save_workfile.py index 9662f31922..517f5fd17f 100644 --- a/openpype/hosts/substancepainter/plugins/publish/save_workfile.py +++ b/openpype/hosts/substancepainter/plugins/publish/save_workfile.py @@ -20,7 +20,7 @@ class SaveCurrentWorkfile(pyblish.api.ContextPlugin): if context.data["currentFile"] != current: raise KnownPublishError("Workfile has changed during publishing!") - if host.has_unsaved_changes(): + if host.workfile_has_unsaved_changes(): self.log.info("Saving current file: {}".format(current)) host.save_workfile() else: