From b012b0aaeb21d6ffe1e36e90a9743567694c196d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 22 May 2023 20:14:39 +0800 Subject: [PATCH 01/69] 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/69] 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/69] 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 381602de484e91047c359fd482c8edf40795993f Mon Sep 17 00:00:00 2001 From: jbeaulieu Date: Fri, 26 May 2023 18:53:17 -0400 Subject: [PATCH 04/69] Create node with inpanel already False as opposed to setting after UI update --- openpype/hosts/nuke/api/lib.py | 57 ++++++++++++------- .../nuke/plugins/load/load_camera_abc.py | 2 - openpype/hosts/nuke/plugins/load/load_clip.py | 7 +-- .../hosts/nuke/plugins/load/load_effects.py | 7 +-- .../nuke/plugins/load/load_effects_ip.py | 7 +-- .../hosts/nuke/plugins/load/load_image.py | 7 +-- .../hosts/nuke/plugins/load/load_model.py | 3 - .../nuke/plugins/load/load_script_precomp.py | 7 +-- 8 files changed, 51 insertions(+), 46 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index a439142051..8a75da25a0 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -551,7 +551,9 @@ def add_write_node_legacy(name, **kwarg): w = nuke.createNode( "Write", - "name {}".format(name)) + "name {}".format(name), + inpanel=False + ) w["file"].setValue(kwarg["file"]) @@ -587,7 +589,9 @@ def add_write_node(name, file_path, knobs, **kwarg): w = nuke.createNode( "Write", - "name {}".format(name)) + "name {}".format(name), + inpanel=False + ) w["file"].setValue(file_path) @@ -1190,8 +1194,10 @@ def create_prenodes( # create node now_node = nuke.createNode( - nodeclass, "name {}".format(name)) - now_node.hideControlPanel() + nodeclass, + "name {}".format(name), + inpanel=False + ) # add for dependency linking for_dependency[name] = { @@ -1320,12 +1326,17 @@ def create_write_node( input_name = str(input.name()).replace(" ", "") # if connected input node was defined prev_node = nuke.createNode( - "Input", "name {}".format(input_name)) + "Input", + "name {}".format(input_name), + inpanel=False + ) else: # generic input node connected to nothing prev_node = nuke.createNode( - "Input", "name {}".format("rgba")) - prev_node.hideControlPanel() + "Input", + "name {}".format("rgba"), + inpanel=False + ) # creating pre-write nodes `prenodes` last_prenode = create_prenodes( @@ -1345,15 +1356,13 @@ def create_write_node( imageio_writes["knobs"], **data ) - write_node.hideControlPanel() # connect to previous node now_node.setInput(0, prev_node) # switch actual node to previous prev_node = now_node - now_node = nuke.createNode("Output", "name Output1") - now_node.hideControlPanel() + now_node = nuke.createNode("Output", "name Output1", inpanel=False) # connect to previous node now_node.setInput(0, prev_node) @@ -1522,8 +1531,10 @@ def create_write_node_legacy( else: # generic input node connected to nothing prev_node = nuke.createNode( - "Input", "name {}".format("rgba")) - prev_node.hideControlPanel() + "Input", + "name {}".format("rgba"), + inpanel=False + ) # creating pre-write nodes `prenodes` if prenodes: for node in prenodes: @@ -1535,8 +1546,10 @@ def create_write_node_legacy( # create node now_node = nuke.createNode( - klass, "name {}".format(pre_node_name)) - now_node.hideControlPanel() + klass, + "name {}".format(pre_node_name), + inpanel=False + ) # add data to knob for _knob in knobs: @@ -1566,14 +1579,18 @@ def create_write_node_legacy( if isinstance(dependent, (tuple or list)): for i, node_name in enumerate(dependent): input_node = nuke.createNode( - "Input", "name {}".format(node_name)) - input_node.hideControlPanel() + "Input", + "name {}".format(node_name), + inpanel=False + ) now_node.setInput(1, input_node) elif isinstance(dependent, str): input_node = nuke.createNode( - "Input", "name {}".format(node_name)) - input_node.hideControlPanel() + "Input", + "name {}".format(node_name), + inpanel=False + ) now_node.setInput(0, input_node) else: @@ -1588,15 +1605,13 @@ def create_write_node_legacy( "inside_{}".format(name), **_data ) - write_node.hideControlPanel() # connect to previous node now_node.setInput(0, prev_node) # switch actual node to previous prev_node = now_node - now_node = nuke.createNode("Output", "name Output1") - now_node.hideControlPanel() + now_node = nuke.createNode("Output", "name Output1", inpanel=False) # connect to previous node now_node.setInput(0, prev_node) diff --git a/openpype/hosts/nuke/plugins/load/load_camera_abc.py b/openpype/hosts/nuke/plugins/load/load_camera_abc.py index 11cc63d25c..40822c9eb7 100644 --- a/openpype/hosts/nuke/plugins/load/load_camera_abc.py +++ b/openpype/hosts/nuke/plugins/load/load_camera_abc.py @@ -66,8 +66,6 @@ class AlembicCameraLoader(load.LoaderPlugin): object_name, file), inpanel=False ) - # hide property panel - camera_node.hideControlPanel() camera_node.forceValidate() camera_node["frame_rate"].setValue(float(fps)) diff --git a/openpype/hosts/nuke/plugins/load/load_clip.py b/openpype/hosts/nuke/plugins/load/load_clip.py index cb3da79ef5..ee74582544 100644 --- a/openpype/hosts/nuke/plugins/load/load_clip.py +++ b/openpype/hosts/nuke/plugins/load/load_clip.py @@ -144,10 +144,9 @@ class LoadClip(plugin.NukeLoader): # Create the Loader with the filename path set read_node = nuke.createNode( "Read", - "name {}".format(read_name)) - - # hide property panel - read_node.hideControlPanel() + "name {}".format(read_name), + inpanel=False + ) # to avoid multiple undo steps for rest of process # we will switch off undo-ing diff --git a/openpype/hosts/nuke/plugins/load/load_effects.py b/openpype/hosts/nuke/plugins/load/load_effects.py index d49f87a094..eb1c905c4d 100644 --- a/openpype/hosts/nuke/plugins/load/load_effects.py +++ b/openpype/hosts/nuke/plugins/load/load_effects.py @@ -88,10 +88,9 @@ class LoadEffects(load.LoaderPlugin): GN = nuke.createNode( "Group", - "name {}_1".format(object_name)) - - # hide property panel - GN.hideControlPanel() + "name {}_1".format(object_name), + inpanel=False + ) # adding content to the group node with GN: diff --git a/openpype/hosts/nuke/plugins/load/load_effects_ip.py b/openpype/hosts/nuke/plugins/load/load_effects_ip.py index bfe32c1ed9..03be8654ed 100644 --- a/openpype/hosts/nuke/plugins/load/load_effects_ip.py +++ b/openpype/hosts/nuke/plugins/load/load_effects_ip.py @@ -89,10 +89,9 @@ class LoadEffectsInputProcess(load.LoaderPlugin): GN = nuke.createNode( "Group", - "name {}_1".format(object_name)) - - # hide property panel - GN.hideControlPanel() + "name {}_1".format(object_name), + inpanel=False + ) # adding content to the group node with GN: diff --git a/openpype/hosts/nuke/plugins/load/load_image.py b/openpype/hosts/nuke/plugins/load/load_image.py index f82ee4db88..0a79ddada7 100644 --- a/openpype/hosts/nuke/plugins/load/load_image.py +++ b/openpype/hosts/nuke/plugins/load/load_image.py @@ -119,10 +119,9 @@ class LoadImage(load.LoaderPlugin): with viewer_update_and_undo_stop(): r = nuke.createNode( "Read", - "name {}".format(read_name)) - - # hide property panel - r.hideControlPanel() + "name {}".format(read_name), + inpanel=False + ) r["file"].setValue(file) diff --git a/openpype/hosts/nuke/plugins/load/load_model.py b/openpype/hosts/nuke/plugins/load/load_model.py index f968da8475..36781993ea 100644 --- a/openpype/hosts/nuke/plugins/load/load_model.py +++ b/openpype/hosts/nuke/plugins/load/load_model.py @@ -65,9 +65,6 @@ class AlembicModelLoader(load.LoaderPlugin): inpanel=False ) - # hide property panel - model_node.hideControlPanel() - model_node.forceValidate() # Ensure all items are imported and selected. diff --git a/openpype/hosts/nuke/plugins/load/load_script_precomp.py b/openpype/hosts/nuke/plugins/load/load_script_precomp.py index 53e9a76003..b74fdf481a 100644 --- a/openpype/hosts/nuke/plugins/load/load_script_precomp.py +++ b/openpype/hosts/nuke/plugins/load/load_script_precomp.py @@ -70,10 +70,9 @@ class LinkAsGroup(load.LoaderPlugin): # P = nuke.nodes.LiveGroup("file {}".format(file)) P = nuke.createNode( "Precomp", - "file {}".format(file)) - - # hide property panel - P.hideControlPanel() + "file {}".format(file), + inpanel=False + ) # Set colorspace defined in version data colorspace = context["version"]["data"].get("colorspace", None) From 277fd3e3423f636240867d81167f0c2c63e22b48 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 15:09:09 +0800 Subject: [PATCH 05/69] 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 06/69] 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 07/69] 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 08/69] 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 09/69] 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 10/69] 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 11/69] 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 12/69] 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 13/69] 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 14/69] 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 15/69] 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 16/69] 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 17/69] 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 18/69] 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 19/69] 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 20/69] 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 21/69] 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 53dac8b0a8e1893afc9c1720d6c17ca9a65ec911 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 28 Jun 2023 13:12:34 +0800 Subject: [PATCH 22/69] maxscript's conversion of bool to python --- openpype/hosts/max/api/plugin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/max/api/plugin.py b/openpype/hosts/max/api/plugin.py index 71a0b94e1f..69a495f5ae 100644 --- a/openpype/hosts/max/api/plugin.py +++ b/openpype/hosts/max/api/plugin.py @@ -172,7 +172,7 @@ class MaxCreator(Creator, MaxCreatorBase): # Setting the property rt.setProperty( instance_node.openPypeData, "all_handles", node_list) - + self.log.debug(f"{instance}") self._add_instance_to_context(instance) imprint(instance_node.name, instance.data_to_store()) @@ -184,6 +184,7 @@ class MaxCreator(Creator, MaxCreatorBase): created_instance = CreatedInstance.from_existing( read(rt.GetNodeByName(instance)), self ) + self.log.debug(f"{created_instance}") self._add_instance_to_context(created_instance) def update_instances(self, update_list): From 5a228d4d5192825081225cdaef23d9cbf20397bb Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 28 Jun 2023 13:21:37 +0800 Subject: [PATCH 23/69] maxscript's conversion of bool to python --- openpype/hosts/max/api/lib.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 1d53802ecf..c1e67409a2 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -78,7 +78,13 @@ def read(container) -> dict: value.startswith(JSON_PREFIX): with contextlib.suppress(json.JSONDecodeError): value = json.loads(value[len(JSON_PREFIX):]) - data[key.strip()] = value + if key.strip() == "active": + if value == "true": + data[key.strip()] = True + else: + data[key.strip()] = False + else: + data[key.strip()] = value data["instance_node"] = container.Name From eb63b4bae1291006071b3689735abe5e4b1d7829 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 28 Jun 2023 13:22:35 +0800 Subject: [PATCH 24/69] remove unnecessary debug check --- openpype/hosts/max/api/plugin.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/max/api/plugin.py b/openpype/hosts/max/api/plugin.py index 69a495f5ae..08e41df554 100644 --- a/openpype/hosts/max/api/plugin.py +++ b/openpype/hosts/max/api/plugin.py @@ -172,7 +172,6 @@ class MaxCreator(Creator, MaxCreatorBase): # Setting the property rt.setProperty( instance_node.openPypeData, "all_handles", node_list) - self.log.debug(f"{instance}") self._add_instance_to_context(instance) imprint(instance_node.name, instance.data_to_store()) @@ -184,7 +183,6 @@ class MaxCreator(Creator, MaxCreatorBase): created_instance = CreatedInstance.from_existing( read(rt.GetNodeByName(instance)), self ) - self.log.debug(f"{created_instance}") self._add_instance_to_context(created_instance) def update_instances(self, update_list): From 3c4c922b4f5e66874ccc886ecddff0d02528afce Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 28 Jun 2023 13:23:22 +0800 Subject: [PATCH 25/69] restore the plugin.py --- openpype/hosts/max/api/plugin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/max/api/plugin.py b/openpype/hosts/max/api/plugin.py index 08e41df554..71a0b94e1f 100644 --- a/openpype/hosts/max/api/plugin.py +++ b/openpype/hosts/max/api/plugin.py @@ -172,6 +172,7 @@ class MaxCreator(Creator, MaxCreatorBase): # Setting the property rt.setProperty( instance_node.openPypeData, "all_handles", node_list) + self._add_instance_to_context(instance) imprint(instance_node.name, instance.data_to_store()) From 04ec40134329694e72d7421e13f60744f824af1a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 28 Jun 2023 15:52:37 +0800 Subject: [PATCH 26/69] roy's comment --- openpype/hosts/max/api/lib.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index c1e67409a2..879f0abfa4 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -78,13 +78,15 @@ def read(container) -> dict: value.startswith(JSON_PREFIX): with contextlib.suppress(json.JSONDecodeError): value = json.loads(value[len(JSON_PREFIX):]) - if key.strip() == "active": - if value == "true": - data[key.strip()] = True - else: - data[key.strip()] = False - else: - data[key.strip()] = value + + # default value behavior + # convert maxscript boolean values + if value == "true": + value = True + elif value == "false": + value = False + + data[key.strip()] = value data["instance_node"] = container.Name From a93d2b9c996835200ce9e03509e9533655caab85 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 29 Jun 2023 10:13:37 +0200 Subject: [PATCH 27/69] 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 0b6f9dc8f2e872becb2d058a593dc0d1e02842e0 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 30 Jun 2023 22:53:21 +0200 Subject: [PATCH 28/69] 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 29/69] 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 30/69] 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 31/69] 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 32/69] 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 33/69] 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 34/69] 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 35/69] 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 36/69] 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 37/69] 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 38/69] 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 39/69] 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 40/69] 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 76b6fed6a7fec021be19f0ae104f1499781b8f5e Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov Date: Mon, 3 Jul 2023 03:41:37 +0300 Subject: [PATCH 41/69] 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 42/69] 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 09f0d183d8915efe72ad00056eee4955ced4cd09 Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov Date: Mon, 3 Jul 2023 15:40:16 +0300 Subject: [PATCH 43/69] 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 a0fb1d49cad2da140e3b3ff9a791b37bdabaa199 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Mon, 3 Jul 2023 17:36:48 +0300 Subject: [PATCH 44/69] add select invalid action --- .../publish/validate_sop_output_node.py | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_sop_output_node.py b/openpype/hosts/houdini/plugins/publish/validate_sop_output_node.py index ed7f438729..74f45c0925 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_sop_output_node.py +++ b/openpype/hosts/houdini/plugins/publish/validate_sop_output_node.py @@ -1,7 +1,13 @@ # -*- coding: utf-8 -*- import pyblish.api from openpype.pipeline import PublishValidationError +from openpype.pipeline.publish import RepairAction +import hou + +class SelectInvalidAction(RepairAction): + label = "Select Invalid ROP" + icon = "mdi.cursor-default-click" class ValidateSopOutputNode(pyblish.api.InstancePlugin): """Validate the instance SOP Output Node. @@ -19,6 +25,7 @@ class ValidateSopOutputNode(pyblish.api.InstancePlugin): families = ["pointcache", "vdbcache"] hosts = ["houdini"] label = "Validate Output Node" + actions = [SelectInvalidAction] def process(self, instance): @@ -31,9 +38,6 @@ class ValidateSopOutputNode(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance): - - import hou - output_node = instance.data.get("output_node") if output_node is None: @@ -81,3 +85,19 @@ class ValidateSopOutputNode(pyblish.api.InstancePlugin): "Output node `%s` has no geometry data." % output_node.path() ) return [output_node.path()] + + @classmethod + def repair(cls, instance): + """Select Invalid ROP. + + It's used to select invalid ROP which tells the + artist which ROP node need to be fixed! + """ + + rop_node = hou.node(instance.data["instance_node"]) + rop_node.setSelected(True, clear_all_selected=True) + + cls.log.debug( + '%s has been selected' + % rop_node.path() + ) From 9ccbaed576bb93b076c8d05664cb2a6bf3e289b9 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 3 Jul 2023 23:12:59 +0800 Subject: [PATCH 45/69] 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 6983503468b03490e53089e71a45d86780dfc5a6 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Mon, 3 Jul 2023 18:16:23 +0300 Subject: [PATCH 46/69] fix lint problem --- .../hosts/houdini/plugins/publish/validate_sop_output_node.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/houdini/plugins/publish/validate_sop_output_node.py b/openpype/hosts/houdini/plugins/publish/validate_sop_output_node.py index 74f45c0925..e8fb11a51c 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_sop_output_node.py +++ b/openpype/hosts/houdini/plugins/publish/validate_sop_output_node.py @@ -5,6 +5,7 @@ from openpype.pipeline.publish import RepairAction import hou + class SelectInvalidAction(RepairAction): label = "Select Invalid ROP" icon = "mdi.cursor-default-click" From 2362c4114e227604974920216997bfc35fe4cfd4 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 4 Jul 2023 17:44:58 +0800 Subject: [PATCH 47/69] 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 73ecfe88e8773f5178d72785ac1771b83dde3b7f Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Tue, 4 Jul 2023 13:27:31 +0300 Subject: [PATCH 48/69] change action name to Select ROP --- .../plugins/publish/validate_sop_output_node.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_sop_output_node.py b/openpype/hosts/houdini/plugins/publish/validate_sop_output_node.py index e8fb11a51c..0d2aa64df6 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_sop_output_node.py +++ b/openpype/hosts/houdini/plugins/publish/validate_sop_output_node.py @@ -6,8 +6,8 @@ from openpype.pipeline.publish import RepairAction import hou -class SelectInvalidAction(RepairAction): - label = "Select Invalid ROP" +class SelectROPAction(RepairAction): + label = "Select ROP" icon = "mdi.cursor-default-click" class ValidateSopOutputNode(pyblish.api.InstancePlugin): @@ -26,7 +26,7 @@ class ValidateSopOutputNode(pyblish.api.InstancePlugin): families = ["pointcache", "vdbcache"] hosts = ["houdini"] label = "Validate Output Node" - actions = [SelectInvalidAction] + actions = [SelectROPAction] def process(self, instance): @@ -89,10 +89,10 @@ class ValidateSopOutputNode(pyblish.api.InstancePlugin): @classmethod def repair(cls, instance): - """Select Invalid ROP. + """Select ROP. - It's used to select invalid ROP which tells the - artist which ROP node need to be fixed! + It's used to select the associated ROP for the selected instance + which tells the artist which ROP node need to be fixed! """ rop_node = hou.node(instance.data["instance_node"]) From 379cf0f76976843d966e4739d1fa6446d0524774 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Tue, 4 Jul 2023 13:29:24 +0300 Subject: [PATCH 49/69] add SelectInvalidAction --- .../plugins/publish/validate_sop_output_node.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_sop_output_node.py b/openpype/hosts/houdini/plugins/publish/validate_sop_output_node.py index 0d2aa64df6..834bc39a24 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_sop_output_node.py +++ b/openpype/hosts/houdini/plugins/publish/validate_sop_output_node.py @@ -2,6 +2,7 @@ import pyblish.api from openpype.pipeline import PublishValidationError from openpype.pipeline.publish import RepairAction +from openpype.hosts.houdini.api.action import SelectInvalidAction import hou @@ -26,7 +27,7 @@ class ValidateSopOutputNode(pyblish.api.InstancePlugin): families = ["pointcache", "vdbcache"] hosts = ["houdini"] label = "Validate Output Node" - actions = [SelectROPAction] + actions = [SelectROPAction, SelectInvalidAction] def process(self, instance): @@ -48,7 +49,7 @@ class ValidateSopOutputNode(pyblish.api.InstancePlugin): "Ensure a valid SOP output path is set." % node.path() ) - return [node.path()] + return [node] # Output node must be a Sop node. if not isinstance(output_node, hou.SopNode): @@ -58,7 +59,7 @@ class ValidateSopOutputNode(pyblish.api.InstancePlugin): "instead found category type: %s" % (output_node.path(), output_node.type().category().name()) ) - return [output_node.path()] + return [output_node] # For the sake of completeness also assert the category type # is Sop to avoid potential edge case scenarios even though @@ -78,14 +79,14 @@ class ValidateSopOutputNode(pyblish.api.InstancePlugin): except hou.Error as exc: cls.log.error("Cook failed: %s" % exc) cls.log.error(output_node.errors()[0]) - return [output_node.path()] + return [output_node] # Ensure the output node has at least Geometry data if not output_node.geometry(): cls.log.error( "Output node `%s` has no geometry data." % output_node.path() ) - return [output_node.path()] + return [output_node] @classmethod def repair(cls, instance): From 8befb04439dd4cd0a815b88aef13005a4d76b8f9 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 4 Jul 2023 18:31:41 +0800 Subject: [PATCH 50/69] 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 51/69] 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 52/69] 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 612b85a703f05a460ecda0285ab441ef72334c39 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Tue, 4 Jul 2023 17:08:14 +0300 Subject: [PATCH 53/69] move SelectRopAction to api.actions --- openpype/hosts/houdini/api/action.py | 43 +++++++++++++++++++ .../publish/validate_sop_output_node.py | 26 ++--------- 2 files changed, 47 insertions(+), 22 deletions(-) diff --git a/openpype/hosts/houdini/api/action.py b/openpype/hosts/houdini/api/action.py index 27e8ce55bb..b6879bb276 100644 --- a/openpype/hosts/houdini/api/action.py +++ b/openpype/hosts/houdini/api/action.py @@ -44,3 +44,46 @@ class SelectInvalidAction(pyblish.api.Action): node.setCurrent(True) else: self.log.info("No invalid nodes found.") + + +class SelectROPAction(pyblish.api.Action): + """Select ROP. + + It's used to select the associated ROPs with all errored instances + not necessarily the ones that errored on the plugin we're running the action on. + """ + + label = "Select ROP" + on = "failed" # This action is only available on a failed plug-in + icon = "mdi.cursor-default-click" + + def process(self, context, plugin): + errored_instances = get_errored_instances_from_context(context) + + # Apply pyblish.logic to get the instances for the plug-in + instances = pyblish.api.instances_by_plugin(errored_instances, plugin) + + # Get the invalid nodes for the plug-ins + self.log.info("Finding ROP nodes..") + rop_nodes = list() + for instance in instances: + node_path = instance.data.get("instance_node") + if not node_path: + continue + + node = hou.node(node_path) + if not node: + continue + + rop_nodes.append(node) + + hou.clearAllSelected() + if rop_nodes: + self.log.info("Selecting ROP nodes: {}".format( + ", ".join(node.path() for node in rop_nodes) + )) + for node in rop_nodes: + node.setSelected(True) + node.setCurrent(True) + else: + self.log.info("No ROP nodes found.") diff --git a/openpype/hosts/houdini/plugins/publish/validate_sop_output_node.py b/openpype/hosts/houdini/plugins/publish/validate_sop_output_node.py index 834bc39a24..d9dee38680 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_sop_output_node.py +++ b/openpype/hosts/houdini/plugins/publish/validate_sop_output_node.py @@ -1,16 +1,14 @@ # -*- coding: utf-8 -*- import pyblish.api from openpype.pipeline import PublishValidationError -from openpype.pipeline.publish import RepairAction -from openpype.hosts.houdini.api.action import SelectInvalidAction +from openpype.hosts.houdini.api.action import ( + SelectInvalidAction, + SelectROPAction, +) import hou -class SelectROPAction(RepairAction): - label = "Select ROP" - icon = "mdi.cursor-default-click" - class ValidateSopOutputNode(pyblish.api.InstancePlugin): """Validate the instance SOP Output Node. @@ -87,19 +85,3 @@ class ValidateSopOutputNode(pyblish.api.InstancePlugin): "Output node `%s` has no geometry data." % output_node.path() ) return [output_node] - - @classmethod - def repair(cls, instance): - """Select ROP. - - It's used to select the associated ROP for the selected instance - which tells the artist which ROP node need to be fixed! - """ - - rop_node = hou.node(instance.data["instance_node"]) - rop_node.setSelected(True, clear_all_selected=True) - - cls.log.debug( - '%s has been selected' - % rop_node.path() - ) From d61bd762049acba4e6fbc0445cb031dde5f452e8 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Tue, 4 Jul 2023 17:11:21 +0300 Subject: [PATCH 54/69] fix lint problems --- openpype/hosts/houdini/api/action.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/api/action.py b/openpype/hosts/houdini/api/action.py index b6879bb276..a9afe38931 100644 --- a/openpype/hosts/houdini/api/action.py +++ b/openpype/hosts/houdini/api/action.py @@ -49,8 +49,7 @@ class SelectInvalidAction(pyblish.api.Action): class SelectROPAction(pyblish.api.Action): """Select ROP. - It's used to select the associated ROPs with all errored instances - not necessarily the ones that errored on the plugin we're running the action on. + It's used to select the associated ROPs with the errored instances. """ label = "Select ROP" From ae05aba0891e7107335b66309ce825eeae8af675 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 5 Jul 2023 03:32:31 +0000 Subject: [PATCH 55/69] 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 ba877956b94b1663ce6d38168eea48da8dd6bfca Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 5 Jul 2023 09:17:53 +0100 Subject: [PATCH 56/69] Fix collecting arnold prefix when none --- openpype/hosts/maya/api/lib_renderproducts.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py index a6bcd003a5..4f52372f06 100644 --- a/openpype/hosts/maya/api/lib_renderproducts.py +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -528,6 +528,9 @@ class RenderProductsArnold(ARenderProducts): def get_renderer_prefix(self): prefix = super(RenderProductsArnold, self).get_renderer_prefix() + if prefix is None: + return "" + merge_aovs = self._get_attr("defaultArnoldDriver.mergeAOVs") if not merge_aovs and "" not in prefix.lower(): # When Merge AOVs is disabled and token not present From cc7a1c0e72c20d1de61f8416246ca6b85ffe404b Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 5 Jul 2023 09:21:12 +0100 Subject: [PATCH 57/69] 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 58/69] 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 59/69] 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 60/69] 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 61/69] 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: From 9dbba9394b97562025a83cbc5075e17f22dedf73 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 5 Jul 2023 14:06:58 +0300 Subject: [PATCH 62/69] update action with roy's PR --- openpype/hosts/houdini/api/action.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/houdini/api/action.py b/openpype/hosts/houdini/api/action.py index a9afe38931..eeb9cfda62 100644 --- a/openpype/hosts/houdini/api/action.py +++ b/openpype/hosts/houdini/api/action.py @@ -57,15 +57,12 @@ class SelectROPAction(pyblish.api.Action): icon = "mdi.cursor-default-click" def process(self, context, plugin): - errored_instances = get_errored_instances_from_context(context) - - # Apply pyblish.logic to get the instances for the plug-in - instances = pyblish.api.instances_by_plugin(errored_instances, plugin) + errored_instances = get_errored_instances_from_context(context, plugin) # Get the invalid nodes for the plug-ins self.log.info("Finding ROP nodes..") rop_nodes = list() - for instance in instances: + for instance in errored_instances: node_path = instance.data.get("instance_node") if not node_path: continue From 974d70869300015c9044a1a4b043b50c247ed670 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 5 Jul 2023 12:41:26 +0100 Subject: [PATCH 63/69] Revert "Fix collecting arnold prefix when none" This reverts commit ba877956b94b1663ce6d38168eea48da8dd6bfca. --- openpype/hosts/maya/api/lib_renderproducts.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py index 4f52372f06..a6bcd003a5 100644 --- a/openpype/hosts/maya/api/lib_renderproducts.py +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -528,9 +528,6 @@ class RenderProductsArnold(ARenderProducts): def get_renderer_prefix(self): prefix = super(RenderProductsArnold, self).get_renderer_prefix() - if prefix is None: - return "" - merge_aovs = self._get_attr("defaultArnoldDriver.mergeAOVs") if not merge_aovs and "" not in prefix.lower(): # When Merge AOVs is disabled and token not present From 558cd4fe6818cb708c46d2b3f02f6a4125e8b355 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 5 Jul 2023 12:42:16 +0100 Subject: [PATCH 64/69] Use BigRoy soluiton --- openpype/hosts/maya/api/lib_renderproducts.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py index a6bcd003a5..7bfb53d500 100644 --- a/openpype/hosts/maya/api/lib_renderproducts.py +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -274,12 +274,14 @@ class ARenderProducts: "Unsupported renderer {}".format(self.renderer) ) + # Note: When this attribute is never set (e.g. on maya launch) then + # this can return None even though it is a string attribute prefix = self._get_attr(prefix_attr) if not prefix: # Fall back to scene name by default - log.debug("Image prefix not set, using ") - file_prefix = "" + log.warning("Image prefix not set, using ") + prefix = "" return prefix From 2b23b42da65b6943a6b46253816422f71c7ac8c6 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 5 Jul 2023 14:11:20 +0200 Subject: [PATCH 65/69] RepairAction and SelectInvalidAction actually filter to instances that failed on the exact plugin - not on "any failure" (#5240) --- openpype/action.py | 12 +++++------- openpype/hosts/blender/api/action.py | 6 +++--- openpype/hosts/fusion/api/action.py | 8 +++----- openpype/hosts/houdini/api/action.py | 8 +++----- openpype/hosts/maya/api/action.py | 8 +++----- openpype/hosts/nuke/api/actions.py | 8 +++----- .../nuke/plugins/publish/validate_rendered_frames.py | 10 ++-------- openpype/hosts/resolve/api/action.py | 8 +++----- openpype/pipeline/publish/lib.py | 7 ++++++- openpype/pipeline/publish/publish_plugins.py | 8 +++----- 10 files changed, 34 insertions(+), 49 deletions(-) diff --git a/openpype/action.py b/openpype/action.py index 15c96404b6..6114c65fd4 100644 --- a/openpype/action.py +++ b/openpype/action.py @@ -49,7 +49,7 @@ def deprecated(new_destination): @deprecated("openpype.pipeline.publish.get_errored_instances_from_context") -def get_errored_instances_from_context(context): +def get_errored_instances_from_context(context, plugin=None): """ Deprecated: Since 3.14.* will be removed in 3.16.* or later. @@ -57,7 +57,7 @@ def get_errored_instances_from_context(context): from openpype.pipeline.publish import get_errored_instances_from_context - return get_errored_instances_from_context(context) + return get_errored_instances_from_context(context, plugin=plugin) @deprecated("openpype.pipeline.publish.get_errored_plugins_from_context") @@ -97,11 +97,9 @@ class RepairAction(pyblish.api.Action): # Get the errored instances self.log.info("Finding failed instances..") - errored_instances = get_errored_instances_from_context(context) - - # Apply pyblish.logic to get the instances for the plug-in - instances = pyblish.api.instances_by_plugin(errored_instances, plugin) - for instance in instances: + errored_instances = get_errored_instances_from_context(context, + plugin=plugin) + for instance in errored_instances: plugin.repair(instance) diff --git a/openpype/hosts/blender/api/action.py b/openpype/hosts/blender/api/action.py index fe0833e39f..dc49d6d9ae 100644 --- a/openpype/hosts/blender/api/action.py +++ b/openpype/hosts/blender/api/action.py @@ -12,13 +12,13 @@ class SelectInvalidAction(pyblish.api.Action): icon = "search" def process(self, context, plugin): - errored_instances = get_errored_instances_from_context(context) - instances = pyblish.api.instances_by_plugin(errored_instances, plugin) + errored_instances = get_errored_instances_from_context(context, + plugin=plugin) # Get the invalid nodes for the plug-ins self.log.info("Finding invalid nodes...") invalid = list() - for instance in instances: + for instance in errored_instances: invalid_nodes = plugin.get_invalid(instance) if invalid_nodes: if isinstance(invalid_nodes, (list, tuple)): diff --git a/openpype/hosts/fusion/api/action.py b/openpype/hosts/fusion/api/action.py index ff5dd14caa..347d552108 100644 --- a/openpype/hosts/fusion/api/action.py +++ b/openpype/hosts/fusion/api/action.py @@ -18,15 +18,13 @@ class SelectInvalidAction(pyblish.api.Action): icon = "search" # Icon from Awesome Icon def process(self, context, plugin): - errored_instances = get_errored_instances_from_context(context) - - # Apply pyblish.logic to get the instances for the plug-in - instances = pyblish.api.instances_by_plugin(errored_instances, plugin) + errored_instances = get_errored_instances_from_context(context, + plugin=plugin) # Get the invalid nodes for the plug-ins self.log.info("Finding invalid nodes..") invalid = list() - for instance in instances: + for instance in errored_instances: invalid_nodes = plugin.get_invalid(instance) if invalid_nodes: if isinstance(invalid_nodes, (list, tuple)): diff --git a/openpype/hosts/houdini/api/action.py b/openpype/hosts/houdini/api/action.py index 27e8ce55bb..b1519ddd1d 100644 --- a/openpype/hosts/houdini/api/action.py +++ b/openpype/hosts/houdini/api/action.py @@ -17,15 +17,13 @@ class SelectInvalidAction(pyblish.api.Action): def process(self, context, plugin): - errored_instances = get_errored_instances_from_context(context) - - # Apply pyblish.logic to get the instances for the plug-in - instances = pyblish.api.instances_by_plugin(errored_instances, plugin) + errored_instances = get_errored_instances_from_context(context, + plugin=plugin) # Get the invalid nodes for the plug-ins self.log.info("Finding invalid nodes..") invalid = list() - for instance in instances: + for instance in errored_instances: invalid_nodes = plugin.get_invalid(instance) if invalid_nodes: if isinstance(invalid_nodes, (list, tuple)): diff --git a/openpype/hosts/maya/api/action.py b/openpype/hosts/maya/api/action.py index 065fdf3691..3b8e2c1848 100644 --- a/openpype/hosts/maya/api/action.py +++ b/openpype/hosts/maya/api/action.py @@ -111,15 +111,13 @@ class SelectInvalidAction(pyblish.api.Action): except ImportError: raise ImportError("Current host is not Maya") - errored_instances = get_errored_instances_from_context(context) - - # Apply pyblish.logic to get the instances for the plug-in - instances = pyblish.api.instances_by_plugin(errored_instances, plugin) + errored_instances = get_errored_instances_from_context(context, + plugin=plugin) # Get the invalid nodes for the plug-ins self.log.info("Finding invalid nodes..") invalid = list() - for instance in instances: + for instance in errored_instances: invalid_nodes = plugin.get_invalid(instance) if invalid_nodes: if isinstance(invalid_nodes, (list, tuple)): diff --git a/openpype/hosts/nuke/api/actions.py b/openpype/hosts/nuke/api/actions.py index 92b83560da..c955a85acc 100644 --- a/openpype/hosts/nuke/api/actions.py +++ b/openpype/hosts/nuke/api/actions.py @@ -25,15 +25,13 @@ class SelectInvalidAction(pyblish.api.Action): except ImportError: raise ImportError("Current host is not Nuke") - errored_instances = get_errored_instances_from_context(context) - - # Apply pyblish.logic to get the instances for the plug-in - instances = pyblish.api.instances_by_plugin(errored_instances, plugin) + errored_instances = get_errored_instances_from_context(context, + plugin=plugin) # Get the invalid nodes for the plug-ins self.log.info("Finding invalid nodes..") invalid = list() - for instance in instances: + for instance in errored_instances: invalid_nodes = plugin.get_invalid(instance) if invalid_nodes: diff --git a/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py b/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py index 1c22c5b9d0..45c20412c8 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py +++ b/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py @@ -2,6 +2,7 @@ import os import pyblish.api import clique from openpype.pipeline import PublishXmlValidationError +from openpype.pipeline.publish import get_errored_instances_from_context class RepairActionBase(pyblish.api.Action): @@ -11,14 +12,7 @@ class RepairActionBase(pyblish.api.Action): @staticmethod def get_instance(context, plugin): # Get the errored instances - failed = [] - for result in context.data["results"]: - if (result["error"] is not None and result["instance"] is not None - and result["instance"] not in failed): - failed.append(result["instance"]) - - # Apply pyblish.logic to get the instances for the plug-in - return pyblish.api.instances_by_plugin(failed, plugin) + return get_errored_instances_from_context(context, plugin=plugin) def repair_knob(self, instances, state): for instance in instances: diff --git a/openpype/hosts/resolve/api/action.py b/openpype/hosts/resolve/api/action.py index ceedc2cc54..d1dffca7cc 100644 --- a/openpype/hosts/resolve/api/action.py +++ b/openpype/hosts/resolve/api/action.py @@ -27,15 +27,13 @@ class SelectInvalidAction(pyblish.api.Action): except ImportError: raise ImportError("Current host is not Resolve") - errored_instances = get_errored_instances_from_context(context) - - # Apply pyblish.logic to get the instances for the plug-in - instances = pyblish.api.instances_by_plugin(errored_instances, plugin) + errored_instances = get_errored_instances_from_context(context, + plugin=plugin) # Get the invalid nodes for the plug-ins self.log.info("Finding invalid clips..") invalid = list() - for instance in instances: + for instance in errored_instances: invalid_nodes = plugin.get_invalid(instance) if invalid_nodes: if isinstance(invalid_nodes, (list, tuple)): diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index 471be5ddb8..0961d79234 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -577,12 +577,14 @@ def remote_publish(log, close_plugin_name=None, raise_error=False): raise RuntimeError(error_message) -def get_errored_instances_from_context(context): +def get_errored_instances_from_context(context, plugin=None): """Collect failed instances from pyblish context. Args: context (pyblish.api.Context): Publish context where we're looking for failed instances. + plugin (pyblish.api.Plugin): If provided then only consider errors + related to that plug-in. Returns: List[pyblish.lib.Instance]: Instances which failed during processing. @@ -594,6 +596,9 @@ def get_errored_instances_from_context(context): # When instance is None we are on the "context" result continue + if plugin is not None and result.get("plugin") != plugin: + continue + if result["error"]: instances.append(result["instance"]) diff --git a/openpype/pipeline/publish/publish_plugins.py b/openpype/pipeline/publish/publish_plugins.py index 1eec0760a1..ba3be6397e 100644 --- a/openpype/pipeline/publish/publish_plugins.py +++ b/openpype/pipeline/publish/publish_plugins.py @@ -234,11 +234,9 @@ class RepairAction(pyblish.api.Action): # Get the errored instances self.log.debug("Finding failed instances..") - errored_instances = get_errored_instances_from_context(context) - - # Apply pyblish.logic to get the instances for the plug-in - instances = pyblish.api.instances_by_plugin(errored_instances, plugin) - for instance in instances: + errored_instances = get_errored_instances_from_context(context, + plugin=plugin) + for instance in errored_instances: self.log.debug( "Attempting repair for instance: {} ...".format(instance) ) From 587b98d65eb9b3f858a519f9760b8fbcf0c92522 Mon Sep 17 00:00:00 2001 From: Kayla Man <64118225+moonyuet@users.noreply.github.com> Date: Wed, 5 Jul 2023 23:14:58 +0800 Subject: [PATCH 66/69] General: add the os library before os.environ.get (#5249) * add the os library before os.environ.get * move os import into the top --- openpype/pipeline/create/creator_plugins.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index fbb459ab12..947a90ef08 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -1,3 +1,4 @@ +import os import copy import collections From 4d87046f6a4373fa2587de7c9b2537258101461c Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 6 Jul 2023 16:42:05 +0100 Subject: [PATCH 67/69] Fix set_attribute for enum attributes --- openpype/hosts/maya/api/lib.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 8569bbd38f..fca4410ede 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -1522,7 +1522,15 @@ def set_attribute(attribute, value, node): cmds.addAttr(node, longName=attribute, **kwargs) node_attr = "{}.{}".format(node, attribute) - if "dataType" in kwargs: + enum_type = cmds.attributeQuery(attribute, node=node, enum=True) + if enum_type and value_type == "str": + enum_string_values = cmds.attributeQuery( + attribute, node=node, listEnum=True + )[0].split(":") + cmds.setAttr( + "{}.{}".format(node, attribute), enum_string_values.index(value) + ) + elif "dataType" in kwargs: attr_type = kwargs["dataType"] cmds.setAttr(node_attr, value, type=attr_type) else: From 99efc0e735bb3951b127a467e575532e331e25ae Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov <11698866+movalex@users.noreply.github.com> Date: Fri, 7 Jul 2023 14:38:35 +0300 Subject: [PATCH 68/69] rstrip the template string (#5235) --- openpype/pipeline/delivery.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/pipeline/delivery.py b/openpype/pipeline/delivery.py index 500f54040a..ddde45d4da 100644 --- a/openpype/pipeline/delivery.py +++ b/openpype/pipeline/delivery.py @@ -157,6 +157,8 @@ def deliver_single_file( delivery_path = delivery_path.replace("..", ".") # Make sure path is valid for all platforms delivery_path = os.path.normpath(delivery_path.replace("\\", "/")) + # Remove newlines from the end of the string to avoid OSError during copy + delivery_path = delivery_path.rstrip() delivery_folder = os.path.dirname(delivery_path) if not os.path.exists(delivery_folder): From ed91fdde03cabae62acc9afee6582f413ef7a4b2 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 7 Jul 2023 13:51:35 +0200 Subject: [PATCH 69/69] Update scene inventory even if any errors occurred during update (#5252) * Update scene inventory even if any errors occurred during update + re-use logic * Fix code --- openpype/tools/sceneinventory/view.py | 101 ++++++++++++++------------ 1 file changed, 53 insertions(+), 48 deletions(-) diff --git a/openpype/tools/sceneinventory/view.py b/openpype/tools/sceneinventory/view.py index 73d33392b9..57e6e24411 100644 --- a/openpype/tools/sceneinventory/view.py +++ b/openpype/tools/sceneinventory/view.py @@ -1,5 +1,6 @@ import collections import logging +import itertools from functools import partial from qtpy import QtWidgets, QtCore @@ -195,20 +196,17 @@ class SceneInventoryView(QtWidgets.QTreeView): version_name_by_id[version_doc["_id"]] = \ version_doc["name"] + # Specify version per item to update to + update_items = [] + update_versions = [] for item in items: repre_id = item["representation"] version_id = version_id_by_repre_id.get(repre_id) version_name = version_name_by_id.get(version_id) if version_name is not None: - try: - update_container(item, version_name) - except AssertionError: - self._show_version_error_dialog( - version_name, [item] - ) - log.warning("Update failed", exc_info=True) - - self.data_changed.emit() + update_items.append(item) + update_versions.append(version_name) + self._update_containers(update_items, update_versions) update_icon = qtawesome.icon( "fa.asterisk", @@ -225,16 +223,6 @@ class SceneInventoryView(QtWidgets.QTreeView): update_to_latest_action = None if has_outdated or has_loaded_hero_versions: - # update to latest version - def _on_update_to_latest(items): - for item in items: - try: - update_container(item, -1) - except AssertionError: - self._show_version_error_dialog(None, [item]) - log.warning("Update failed", exc_info=True) - self.data_changed.emit() - update_icon = qtawesome.icon( "fa.angle-double-up", color=DEFAULT_COLOR @@ -245,21 +233,11 @@ class SceneInventoryView(QtWidgets.QTreeView): menu ) update_to_latest_action.triggered.connect( - lambda: _on_update_to_latest(items) + lambda: self._update_containers(items, version=-1) ) change_to_hero = None if has_available_hero_version: - # change to hero version - def _on_update_to_hero(items): - for item in items: - try: - update_container(item, HeroVersionType(-1)) - except AssertionError: - self._show_version_error_dialog('hero', [item]) - log.warning("Update failed", exc_info=True) - self.data_changed.emit() - # TODO change icon change_icon = qtawesome.icon( "fa.asterisk", @@ -271,7 +249,8 @@ class SceneInventoryView(QtWidgets.QTreeView): menu ) change_to_hero.triggered.connect( - lambda: _on_update_to_hero(items) + lambda: self._update_containers(items, + version=HeroVersionType(-1)) ) # set version @@ -740,14 +719,7 @@ class SceneInventoryView(QtWidgets.QTreeView): if label: version = versions_by_label[label] - for item in items: - try: - update_container(item, version) - except AssertionError: - self._show_version_error_dialog(version, [item]) - log.warning("Update failed", exc_info=True) - # refresh model when done - self.data_changed.emit() + self._update_containers(items, version) def _show_switch_dialog(self, items): """Display Switch dialog""" @@ -782,9 +754,9 @@ class SceneInventoryView(QtWidgets.QTreeView): Args: version: str or int or None """ - if not version: + if version == -1: version_str = "latest" - elif version == "hero": + elif isinstance(version, HeroVersionType): version_str = "hero" elif isinstance(version, int): version_str = "v{:03d}".format(version) @@ -841,10 +813,43 @@ class SceneInventoryView(QtWidgets.QTreeView): return # Trigger update to latest - for item in outdated_items: - try: - update_container(item, -1) - except AssertionError: - self._show_version_error_dialog(None, [item]) - log.warning("Update failed", exc_info=True) - self.data_changed.emit() + self._update_containers(outdated_items, version=-1) + + def _update_containers(self, items, version): + """Helper to update items to given version (or version per item) + + If at least one item is specified this will always try to refresh + the inventory even if errors occurred on any of the items. + + Arguments: + items (list): Items to update + version (int or list): Version to set to. + This can be a list specifying a version for each item. + Like `update_container` version -1 sets the latest version + and HeroTypeVersion instances set the hero version. + + """ + + if isinstance(version, (list, tuple)): + # We allow a unique version to be specified per item. In that case + # the length must match with the items + assert len(items) == len(version), ( + "Number of items mismatches number of versions: " + "{} items - {} versions".format(len(items), len(version)) + ) + versions = version + else: + # Repeat the same version infinitely + versions = itertools.repeat(version) + + # Trigger update to latest + try: + for item, item_version in zip(items, versions): + try: + update_container(item, item_version) + except AssertionError: + self._show_version_error_dialog(item_version, [item]) + log.warning("Update failed", exc_info=True) + finally: + # Always update the scene inventory view, even if errors occurred + self.data_changed.emit()