From b012b0aaeb21d6ffe1e36e90a9743567694c196d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 22 May 2023 20:14:39 +0800 Subject: [PATCH 001/107] 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 002/107] 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 003/107] 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 004/107] 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 005/107] 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 006/107] 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 007/107] 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 008/107] 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 009/107] 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 010/107] 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 011/107] 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 012/107] 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 013/107] 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 014/107] 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 015/107] 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 016/107] 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 017/107] 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 018/107] 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 019/107] 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 020/107] 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 021/107] 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 022/107] 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 023/107] 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 024/107] 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 025/107] 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 026/107] 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 7a164032e031b8425aacdd2b998bed7617d5ba85 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 28 Jun 2023 18:35:41 +0100 Subject: [PATCH 027/107] Working callback for managing Xgen sidecar files. --- openpype/hosts/maya/api/pipeline.py | 103 ++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index 5323717fa7..27d489418f 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -2,6 +2,7 @@ import os import errno import logging import contextlib +import shutil from maya import utils, cmds, OpenMaya import maya.api.OpenMaya as om @@ -113,6 +114,9 @@ class MayaHost(HostBase, IWorkfileHost, ILoadHost): register_event_callback("taskChanged", on_task_changed) register_event_callback("workfile.open.before", before_workfile_open) register_event_callback("workfile.save.before", before_workfile_save) + register_event_callback( + "workfile.save.before", workfile_save_before_xgen + ) register_event_callback("workfile.save.before", after_workfile_save) def open_workfile(self, filepath): @@ -681,6 +685,105 @@ def before_workfile_save(event): create_workspace_mel(workdir_path, project_name) +def display_warning(message, show_cancel=False): + """Show feedback to user. + + Returns: + bool + """ + + from qtpy import QtWidgets + + accept = QtWidgets.QMessageBox.Ok + if show_cancel: + buttons = accept | QtWidgets.QMessageBox.Cancel + else: + buttons = accept + + state = QtWidgets.QMessageBox.warning( + None, + "", + message, + buttons=buttons, + defaultButton=accept + ) + + return state == accept + + +def workfile_save_before_xgen(event): + current_work_dir = legacy_io.Session["AVALON_WORKDIR"].replace("\\", "/") + expected_work_dir = event.data["workdir_path"].replace("\\", "/") + if current_work_dir == expected_work_dir: + return + + palettes = cmds.ls(type="xgmPalette", long=True) + if not palettes: + return + + import xgenm + + transfers = [] + overwrites = [] + attribute_changes = {} + attrs = ["xgFileName", "xgBaseFile"] + for palette in palettes: + project_path = xgenm.getAttr("xgProjectPath", palette.replace("|", "")) + _, maya_extension = os.path.splitext(event.data["filename"]) + + for attr in attrs: + node_attr = "{}.{}".format(palette, attr) + attr_value = cmds.getAttr(node_attr) + + if not attr_value: + continue + + source = os.path.join(project_path, attr_value) + + attr_value = event.data["filename"].replace( + maya_extension, + "__{}{}".format( + palette.replace("|", "").replace(":", "__"), + os.path.splitext(attr_value)[1] + ) + ) + target = os.path.join(expected_work_dir, attr_value) + + transfers.append((source, target)) + attribute_changes[node_attr] = attr_value + + relative_path = xgenm.getAttr( + "xgDataPath", palette.replace("|", "") + ).split(os.pathsep)[0] + absolute_path = relative_path.replace("${PROJECT}", project_path) + for root, _, files in os.walk(absolute_path): + for f in files: + source = os.path.join(root, f).replace("\\", "/") + target = source.replace(project_path, expected_work_dir + "/") + transfers.append((source, target)) + if os.path.exists(target): + overwrites.append(target) + + # Ask user about overwriting files. + msg = ( + "WARNING! Potential loss of data.\n\n" + "Found duplicate Xgen files in new context.\n" + "Do you want to overwrite?\n\n{}".format("\n".join(overwrites)) + ) + if overwrites: + accept = display_warning(msg, show_cancel=True) + if not accept: + return + + for attribute, value in attribute_changes.items(): + cmds.setAttr(attribute, value, type="string") + + for source, destination in transfers: + if not os.path.exists(os.path.dirname(destination)): + os.makedirs(os.path.dirname(destination)) + shutil.copy(source, destination) + + def after_workfile_save(event): workfile_name = event["filename"] if ( From 25a03628c949df07af76dbedb1b30767f8ffc37f Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 29 Jun 2023 08:49:43 +0100 Subject: [PATCH 028/107] BigRoy feedback --- openpype/hosts/maya/api/pipeline.py | 39 +++++++++++++++++++---------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index 27d489418f..45064d53f9 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -712,6 +712,20 @@ def display_warning(message, show_cancel=False): def workfile_save_before_xgen(event): + """Manage Xgen external files when switching context. + + Xgen has various external files that needs to be unique and relative to the + workfile, so we need to copy and potentially overwrite these files when + switching context. + + Args: + event (Event) - openpype/lib/events.py + """ + if not cmds.pluginInfo("xgenToolkit", query=True, loaded=True): + return + + import xgenm + current_work_dir = legacy_io.Session["AVALON_WORKDIR"].replace("\\", "/") expected_work_dir = event.data["workdir_path"].replace("\\", "/") if current_work_dir == expected_work_dir: @@ -721,14 +735,13 @@ def workfile_save_before_xgen(event): if not palettes: return - import xgenm - transfers = [] overwrites = [] attribute_changes = {} attrs = ["xgFileName", "xgBaseFile"] for palette in palettes: - project_path = xgenm.getAttr("xgProjectPath", palette.replace("|", "")) + sanitized_palette = palette.replace("|", "") + project_path = xgenm.getAttr("xgProjectPath", sanitized_palette) _, maya_extension = os.path.splitext(event.data["filename"]) for attr in attrs: @@ -743,7 +756,7 @@ def workfile_save_before_xgen(event): attr_value = event.data["filename"].replace( maya_extension, "__{}{}".format( - palette.replace("|", "").replace(":", "__"), + sanitized_palette.replace(":", "__"), os.path.splitext(attr_value)[1] ) ) @@ -753,7 +766,7 @@ def workfile_save_before_xgen(event): attribute_changes[node_attr] = attr_value relative_path = xgenm.getAttr( - "xgDataPath", palette.replace("|", "") + "xgDataPath", sanitized_palette ).split(os.pathsep)[0] absolute_path = relative_path.replace("${PROJECT}", project_path) for root, _, files in os.walk(absolute_path): @@ -765,24 +778,24 @@ def workfile_save_before_xgen(event): overwrites.append(target) # Ask user about overwriting files. - msg = ( - "WARNING! Potential loss of data.\n\n" - "Found duplicate Xgen files in new context.\n" - "Do you want to overwrite?\n\n{}".format("\n".join(overwrites)) - ) if overwrites: + msg = ( + "WARNING! Potential loss of data.\n\n" + "Found duplicate Xgen files in new context.\n" + "Do you want to overwrite?\n\n{}".format("\n".join(overwrites)) + ) accept = display_warning(msg, show_cancel=True) if not accept: return - for attribute, value in attribute_changes.items(): - cmds.setAttr(attribute, value, type="string") - for source, destination in transfers: if not os.path.exists(os.path.dirname(destination)): os.makedirs(os.path.dirname(destination)) shutil.copy(source, destination) + for attribute, value in attribute_changes.items(): + cmds.setAttr(attribute, value, type="string") + def after_workfile_save(event): workfile_name = event["filename"] From a93d2b9c996835200ce9e03509e9533655caab85 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 29 Jun 2023 10:13:37 +0200 Subject: [PATCH 029/107] Use Houdini's values directly --- openpype/hosts/houdini/api/lib.py | 23 ++++------------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index a33ba7aad2..a32e9d8d61 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -633,23 +633,8 @@ def evalParmNoFrame(node, parm, pad_character="#"): def get_color_management_preferences(): """Get default OCIO preferences""" - data = { - "config": hou.Color.ocio_configPath() - + return { + "config": hou.Color.ocio_configPath(), + "display": hou.Color.ocio_defaultDisplay(), + "view": hou.Color.ocio_defaultView() } - - # Get default display and view from OCIO - display = hou.Color.ocio_defaultDisplay() - disp_regex = re.compile(r"^(?P.+-)(?P.+)$") - disp_match = disp_regex.match(display) - - view = hou.Color.ocio_defaultView() - view_regex = re.compile(r"^(?P.+- )(?P.+)$") - view_match = view_regex.match(view) - data.update({ - "display": disp_match.group("display"), - "view": view_match.group("view") - - }) - - return data From 7ca847ce247d7278190143885d4cb5a3a6ba5267 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 29 Jun 2023 10:36:41 +0100 Subject: [PATCH 030/107] Change name of warning method. --- openpype/hosts/maya/api/pipeline.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index 8e5c2f9fb0..98ebd9f028 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -685,7 +685,7 @@ def before_workfile_save(event): create_workspace_mel(workdir_path, project_name) -def display_warning(message, show_cancel=False): +def prompt_warning(message, show_cancel=False): """Show feedback to user. Returns: @@ -784,7 +784,7 @@ def workfile_save_before_xgen(event): "Found duplicate Xgen files in new context.\n" "Do you want to overwrite?\n\n{}".format("\n".join(overwrites)) ) - accept = display_warning(msg, show_cancel=True) + accept = prompt_warning(msg, show_cancel=True) if not accept: return From aa5236128b5a8f63cfa0e2495c28ad1adaf61693 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Fri, 30 Jun 2023 08:15:07 +0100 Subject: [PATCH 031/107] Update openpype/hosts/maya/api/pipeline.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/maya/api/pipeline.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index 98ebd9f028..c2e4ffaba0 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -688,9 +688,9 @@ def before_workfile_save(event): def prompt_warning(message, show_cancel=False): """Show feedback to user. - Returns: - bool - """ + Returns: + bool + """ from qtpy import QtWidgets From 2a40984858d20d47a7f99f5b8ca070d0e63159f4 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 30 Jun 2023 08:16:37 +0100 Subject: [PATCH 032/107] import at top --- openpype/hosts/maya/api/pipeline.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index 98ebd9f028..33ab2ac71e 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -8,6 +8,7 @@ from maya import utils, cmds, OpenMaya import maya.api.OpenMaya as om import pyblish.api +from qtpy import QtWidgets from openpype.settings import get_project_settings from openpype.host import ( @@ -691,9 +692,6 @@ def prompt_warning(message, show_cancel=False): Returns: bool """ - - from qtpy import QtWidgets - accept = QtWidgets.QMessageBox.Ok if show_cancel: buttons = accept | QtWidgets.QMessageBox.Cancel From 0033efe5b181f78e4fe13216305933ecaa1fb447 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Fri, 30 Jun 2023 15:08:38 +0200 Subject: [PATCH 033/107] catch assertion error --- openpype/hosts/maya/api/workfile_template_builder.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/workfile_template_builder.py b/openpype/hosts/maya/api/workfile_template_builder.py index e2f30f46d0..ba3435b60d 100644 --- a/openpype/hosts/maya/api/workfile_template_builder.py +++ b/openpype/hosts/maya/api/workfile_template_builder.py @@ -272,7 +272,12 @@ class MayaPlaceholderLoadPlugin(PlaceholderPlugin, PlaceholderLoadMixin): return roots = cmds.sets(container, q=True) - ref_node = get_reference_node(roots) + ref_node = None + try: + ref_node = get_reference_node(roots) + except AssertionError as e: + self.log.info(e.args[0]) + nodes_to_parent = [] for root in roots: if ref_node: From 13820bf99b54dd42e702da8df027497e9932b1f0 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Fri, 30 Jun 2023 15:15:06 +0200 Subject: [PATCH 034/107] indent correction --- openpype/hosts/maya/api/workfile_template_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/workfile_template_builder.py b/openpype/hosts/maya/api/workfile_template_builder.py index ba3435b60d..865f497710 100644 --- a/openpype/hosts/maya/api/workfile_template_builder.py +++ b/openpype/hosts/maya/api/workfile_template_builder.py @@ -274,7 +274,7 @@ class MayaPlaceholderLoadPlugin(PlaceholderPlugin, PlaceholderLoadMixin): roots = cmds.sets(container, q=True) ref_node = None try: - ref_node = get_reference_node(roots) + ref_node = get_reference_node(roots) except AssertionError as e: self.log.info(e.args[0]) From 3e93d163c03048b801b654dcd0400cbecf2bd7f4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 30 Jun 2023 16:11:37 +0200 Subject: [PATCH 035/107] check PyOpenColorIO rather then python version https://github.com/ynput/OpenPype/pull/5212#issuecomment-1614651292 --- openpype/pipeline/colorspace.py | 36 +++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index d5f2624155..244463ad41 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -1,7 +1,6 @@ from copy import deepcopy import re import os -import sys import json import platform import contextlib @@ -237,12 +236,13 @@ def get_data_subprocess(config_path, data_type): return json.loads(return_json_data) -def compatible_python(): - """Only 3.9 or higher can directly use PyOpenColorIO in ocio_wrapper""" - compatible = False - if sys.version_info.major == 3 and sys.version_info.minor >= 9: - compatible = True - return compatible +def compatibility_check(): + """Making sure PyOpenColorIO is importable""" + try: + import PyOpenColorIO + except (ImportError, ModuleNotFoundError): + return False + return True def get_ocio_config_colorspaces(config_path): @@ -257,12 +257,15 @@ def get_ocio_config_colorspaces(config_path): Returns: dict: colorspace and family in couple """ - if compatible_python(): - from ..scripts.ocio_wrapper import _get_colorspace_data - return _get_colorspace_data(config_path) - else: + if not compatibility_check(): + # python environment is not compatible with PyOpenColorIO + # needs to be run in subprocess return get_colorspace_data_subprocess(config_path) + from openpype.scripts.ocio_wrapper import _get_colorspace_data + + return _get_colorspace_data(config_path) + def get_colorspace_data_subprocess(config_path): """Get colorspace data via subprocess @@ -290,12 +293,15 @@ def get_ocio_config_views(config_path): Returns: dict: `display/viewer` and viewer data """ - if compatible_python(): - from ..scripts.ocio_wrapper import _get_views_data - return _get_views_data(config_path) - else: + if not compatibility_check(): + # python environment is not compatible with PyOpenColorIO + # needs to be run in subprocess return get_views_data_subprocess(config_path) + from openpype.scripts.ocio_wrapper import _get_views_data + + return _get_views_data(config_path) + def get_views_data_subprocess(config_path): """Get viewers data via subprocess From 051882bb3e0bf59c265c96718b2c7fa264230191 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 30 Jun 2023 16:16:12 +0200 Subject: [PATCH 036/107] noqa excepetion --- openpype/pipeline/colorspace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 244463ad41..bda14d275d 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -239,7 +239,7 @@ def get_data_subprocess(config_path, data_type): def compatibility_check(): """Making sure PyOpenColorIO is importable""" try: - import PyOpenColorIO + import PyOpenColorIO # noqa: F401 except (ImportError, ModuleNotFoundError): return False return True From 131f8ddd8998372782b80ce73ec936f6254f1358 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Fri, 30 Jun 2023 16:23:11 +0200 Subject: [PATCH 037/107] Update openpype/pipeline/colorspace.py Co-authored-by: Roy Nieterau --- openpype/pipeline/colorspace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index bda14d275d..3f2d4891c1 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -240,7 +240,7 @@ def compatibility_check(): """Making sure PyOpenColorIO is importable""" try: import PyOpenColorIO # noqa: F401 - except (ImportError, ModuleNotFoundError): + except ImportError: return False return True From 8e9f4eb40e4d5595c9786f4b6dc7ffd9a4dcfdc6 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 30 Jun 2023 15:40:31 +0100 Subject: [PATCH 038/107] Log message about overwrites and continue --- openpype/hosts/maya/api/pipeline.py | 35 +++++------------------------ 1 file changed, 5 insertions(+), 30 deletions(-) diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index 8cc4359205..9fab825105 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -686,30 +686,6 @@ def before_workfile_save(event): create_workspace_mel(workdir_path, project_name) -def prompt_warning(message, show_cancel=False): - """Show feedback to user. - - Returns: - bool - """ - - accept = QtWidgets.QMessageBox.Ok - if show_cancel: - buttons = accept | QtWidgets.QMessageBox.Cancel - else: - buttons = accept - - state = QtWidgets.QMessageBox.warning( - None, - "", - message, - buttons=buttons, - defaultButton=accept - ) - - return state == accept - - def workfile_save_before_xgen(event): """Manage Xgen external files when switching context. @@ -778,14 +754,13 @@ def workfile_save_before_xgen(event): # Ask user about overwriting files. if overwrites: - msg = ( + log.warning( "WARNING! Potential loss of data.\n\n" - "Found duplicate Xgen files in new context.\n" - "Do you want to overwrite?\n\n{}".format("\n".join(overwrites)) + "Found duplicate Xgen files in new context.\n{}".format( + "\n".join(overwrites) + ) ) - accept = prompt_warning(msg, show_cancel=True) - if not accept: - return + return for source, destination in transfers: if not os.path.exists(os.path.dirname(destination)): From e7bdf25b9a126bf0e4cffbcc6a2529db1f15af37 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 30 Jun 2023 15:41:39 +0100 Subject: [PATCH 039/107] HOund --- openpype/hosts/maya/api/pipeline.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index 9fab825105..b4042fd3d7 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -8,7 +8,6 @@ from maya import utils, cmds, OpenMaya import maya.api.OpenMaya as om import pyblish.api -from qtpy import QtWidgets from openpype.settings import get_project_settings from openpype.host import ( From 8713759c18ba8e5b617ada2add27bbc154cc6069 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 30 Jun 2023 15:53:08 +0100 Subject: [PATCH 040/107] Hound --- openpype/hosts/maya/api/pipeline.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index b4042fd3d7..9fab825105 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -8,6 +8,7 @@ from maya import utils, cmds, OpenMaya import maya.api.OpenMaya as om import pyblish.api +from qtpy import QtWidgets from openpype.settings import get_project_settings from openpype.host import ( From aaf3a9acfefa4f7f9663c4d96ec273e09a7d4f0c Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 30 Jun 2023 15:53:34 +0100 Subject: [PATCH 041/107] Hound --- openpype/hosts/maya/api/pipeline.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index 9fab825105..b4042fd3d7 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -8,7 +8,6 @@ from maya import utils, cmds, OpenMaya import maya.api.OpenMaya as om import pyblish.api -from qtpy import QtWidgets from openpype.settings import get_project_settings from openpype.host import ( From a2ef4e00138a9e5613516fcba3c10dc45e45f206 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 30 Jun 2023 22:05:45 +0200 Subject: [PATCH 042/107] Restructure `set_colorspace` - Only set `configFilePath` when OCIO env var is not set since it doesn't do anything if OCIO var is set anyway. - Set the Maya 2022+ default OCIO path using the resources path instead of "" to avoid Maya Save File on new file after launch (this also fixes the Save prompt on open last workfile feature with Global color management enabled) - Move all code related to applying the maya settings together after querying the settings - Swap around the `if use_workfile_settings` since the check was reversed - Use `get_current_project_name()` instead of environment vars --- openpype/hosts/maya/api/lib.py | 92 ++++++++++++++++++---------------- 1 file changed, 50 insertions(+), 42 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index ce851d2dbe..f7ddab9f1e 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -3238,36 +3238,21 @@ def iter_shader_edits(relationships, shader_nodes, nodes_by_id, label=None): def set_colorspace(): - """Set Colorspace from project configuration - """ + """Set Colorspace from project configuration""" - # set color spaces for rendering space and view transforms - def _colormanage(**kwargs): - """Wrapper around `cmds.colorManagementPrefs`. - - This logs errors instead of raising an error so color management - settings get applied as much as possible. - - """ - assert len(kwargs) == 1, "Must receive one keyword argument" - try: - cmds.colorManagementPrefs(edit=True, **kwargs) - log.debug("Setting Color Management Preference: {}".format(kwargs)) - except RuntimeError as exc: - log.error(exc) - - project_name = os.getenv("AVALON_PROJECT") + project_name = get_current_project_name() imageio = get_project_settings(project_name)["maya"]["imageio"] # ocio compatibility variables ocio_v2_maya_version = 2022 maya_version = int(cmds.about(version=True)) ocio_v2_support = use_ocio_v2 = maya_version >= ocio_v2_maya_version + is_ocio_set = bool(os.environ.get("OCIO")) - root_dict = {} use_workfile_settings = imageio.get("workfile", {}).get("enabled") - if use_workfile_settings: + root_dict = imageio["workfile"] + else: # TODO: deprecated code from 3.15.5 - remove # Maya 2022+ introduces new OCIO v2 color management settings that # can override the old color management preferences. OpenPype has @@ -3290,40 +3275,63 @@ def set_colorspace(): if not isinstance(root_dict, dict): msg = "set_colorspace(): argument should be dictionary" log.error(msg) + return - else: - root_dict = imageio["workfile"] + # backward compatibility + # TODO: deprecated code from 3.15.5 - remove with deprecated code above + view_name = root_dict.get("viewTransform") + if view_name is None: + view_name = root_dict.get("viewName") log.debug(">> root_dict: {}".format(pformat(root_dict))) + if not root_dict: + return - if root_dict: - # enable color management - cmds.colorManagementPrefs(e=True, cmEnabled=True) - cmds.colorManagementPrefs(e=True, ocioRulesEnabled=True) + # set color spaces for rendering space and view transforms + def _colormanage(**kwargs): + """Wrapper around `cmds.colorManagementPrefs`. - # backward compatibility - # TODO: deprecated code from 3.15.5 - refactor to use new settings - view_name = root_dict.get("viewTransform") - if view_name is None: - view_name = root_dict.get("viewName") + This logs errors instead of raising an error so color management + settings get applied as much as possible. - if use_ocio_v2: - # Use Maya 2022+ default OCIO v2 config + """ + assert len(kwargs) == 1, "Must receive one keyword argument" + try: + cmds.colorManagementPrefs(edit=True, **kwargs) + log.debug("Setting Color Management Preference: {}".format(kwargs)) + except RuntimeError as exc: + log.error(exc) + + # enable color management + cmds.colorManagementPrefs(edit=True, cmEnabled=True) + cmds.colorManagementPrefs(edit=True, ocioRulesEnabled=True) + + if use_ocio_v2: + log.info("Using Maya OCIO v2") + if not is_ocio_set: + # Set the Maya 2022+ default OCIO v2 config file path log.info("Setting default Maya OCIO v2 config") - cmds.colorManagementPrefs(edit=True, configFilePath="") + # Note: Setting "" as value also sets this default however + # introduces a bug where launching a file on startup will prompt + # to save the empty scene before it, so we set using the path. + # This value has been the same for 2022, 2023 and 2024 + path = "/OCIO-configs/Maya2022-default/config.ocio" + cmds.colorManagementPrefs(edit=True, configFilePath=path) - # set rendering space and view transform - _colormanage(renderingSpaceName=root_dict["renderSpace"]) - _colormanage(viewName=view_name) - _colormanage(displayName=root_dict["displayName"]) - else: + # set rendering space and view transform + _colormanage(renderingSpaceName=root_dict["renderSpace"]) + _colormanage(viewName=view_name) + _colormanage(displayName=root_dict["displayName"]) + else: + log.info("Using Maya OCIO v1 (legacy)") + if not is_ocio_set: # Set the Maya default config file path log.info("Setting default Maya OCIO v1 legacy config") cmds.colorManagementPrefs(edit=True, configFilePath="legacy") - # set rendering space and view transform - _colormanage(renderingSpaceName=root_dict["renderSpace"]) - _colormanage(viewTransformName=view_name) + # set rendering space and view transform + _colormanage(renderingSpaceName=root_dict["renderSpace"]) + _colormanage(viewTransformName=view_name) @contextlib.contextmanager From 0b6f9dc8f2e872becb2d058a593dc0d1e02842e0 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 30 Jun 2023 22:53:21 +0200 Subject: [PATCH 043/107] 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 044/107] 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 045/107] 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 046/107] 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 047/107] 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 048/107] 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 049/107] 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 050/107] 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 051/107] 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 052/107] 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 053/107] 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 054/107] 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 055/107] Remove unused comment --- openpype/hosts/maya/api/render_setup_tools.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/maya/api/render_setup_tools.py b/openpype/hosts/maya/api/render_setup_tools.py index 2ad59810d0..a6b46e1e9a 100644 --- a/openpype/hosts/maya/api/render_setup_tools.py +++ b/openpype/hosts/maya/api/render_setup_tools.py @@ -15,7 +15,6 @@ import contextlib from maya import cmds from maya.app.renderSetup.model import renderSetup -# from colorbleed.maya import lib from .lib import pairwise From 344d456214720b6f73c0a693646aa9b39cf0cfd2 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 1 Jul 2023 03:33:14 +0000 Subject: [PATCH 056/107] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index c19af05373..bc3cb93882 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.12-nightly.1" +__version__ = "3.15.12-nightly.2" From d792ca37e3e1f53d48facab858774d7b406df8de Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 1 Jul 2023 03:34:07 +0000 Subject: [PATCH 057/107] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 5d464d0532..652dbb8597 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.15.12-nightly.2 - 3.15.12-nightly.1 - 3.15.11 - 3.15.11-nightly.5 @@ -134,7 +135,6 @@ body: - 3.14.4-nightly.4 - 3.14.4-nightly.3 - 3.14.4-nightly.2 - - 3.14.4-nightly.1 validations: required: true - type: dropdown From 76b6fed6a7fec021be19f0ae104f1499781b8f5e Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov Date: Mon, 3 Jul 2023 03:41:37 +0300 Subject: [PATCH 058/107] 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 059/107] remove unnecessary else statement --- openpype/plugins/load/delivery.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/plugins/load/delivery.py b/openpype/plugins/load/delivery.py index 9509cf3b8c..90e734973b 100644 --- a/openpype/plugins/load/delivery.py +++ b/openpype/plugins/load/delivery.py @@ -155,6 +155,10 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): self._update_selected_label() self._update_template_value() + + btn_delivery.clicked.connect(self.deliver) + dropdown.currentIndexChanged.connect(self._update_template_value) + if not self.dropdown.count(): self.text_area.setVisible(True) error_message = ( @@ -163,9 +167,6 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): ) self.text_area.setText(error_message) self.log.error(error_message.replace("\n", " ")) - else: - btn_delivery.clicked.connect(self.deliver) - dropdown.currentIndexChanged.connect(self._update_template_value) def deliver(self): """Main method to loop through all selected representations""" From fe58cb3c7cc4520b61c8b73c166f6694c5d29e91 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 3 Jul 2023 13:42:00 +0200 Subject: [PATCH 060/107] Enhancement: More descriptive error messages for Loaders (#5227) * More descriptive error messages for Loaders * Restructure error reporting --- openpype/pipeline/load/__init__.py | 4 ++ openpype/pipeline/load/utils.py | 37 +++++++++++++++---- .../tools/sceneinventory/switch_dialog.py | 24 +++++++++--- 3 files changed, 52 insertions(+), 13 deletions(-) diff --git a/openpype/pipeline/load/__init__.py b/openpype/pipeline/load/__init__.py index e9ac0df924..7320a9f0e8 100644 --- a/openpype/pipeline/load/__init__.py +++ b/openpype/pipeline/load/__init__.py @@ -4,6 +4,8 @@ from .utils import ( LoadError, IncompatibleLoaderError, InvalidRepresentationContext, + LoaderSwitchNotImplementedError, + LoaderNotFoundError, get_repres_contexts, get_contexts_for_repre_docs, @@ -55,6 +57,8 @@ __all__ = ( "LoadError", "IncompatibleLoaderError", "InvalidRepresentationContext", + "LoaderSwitchNotImplementedError", + "LoaderNotFoundError", "get_repres_contexts", "get_contexts_for_repre_docs", diff --git a/openpype/pipeline/load/utils.py b/openpype/pipeline/load/utils.py index fefdb8537b..2c40280ccd 100644 --- a/openpype/pipeline/load/utils.py +++ b/openpype/pipeline/load/utils.py @@ -79,6 +79,16 @@ class InvalidRepresentationContext(ValueError): pass +class LoaderSwitchNotImplementedError(NotImplementedError): + """Error when `switch` is used with Loader that has no implementation.""" + pass + + +class LoaderNotFoundError(RuntimeError): + """Error when Loader plugin for a loader name is not found.""" + pass + + def get_repres_contexts(representation_ids, dbcon=None): """Return parenthood context for representation. @@ -432,7 +442,10 @@ def remove_container(container): Loader = _get_container_loader(container) if not Loader: - raise RuntimeError("Can't remove container. See log for details.") + raise LoaderNotFoundError( + "Can't remove container because loader '{}' was not found." + .format(container.get("loader")) + ) loader = Loader(get_representation_context(container["representation"])) return loader.remove(container) @@ -480,7 +493,10 @@ def update_container(container, version=-1): # Run update on the Loader for this container Loader = _get_container_loader(container) if not Loader: - raise RuntimeError("Can't update container. See log for details.") + raise LoaderNotFoundError( + "Can't update container because loader '{}' was not found." + .format(container.get("loader")) + ) loader = Loader(get_representation_context(container["representation"])) return loader.update(container, new_representation) @@ -502,15 +518,18 @@ def switch_container(container, representation, loader_plugin=None): loader_plugin = _get_container_loader(container) if not loader_plugin: - raise RuntimeError("Can't switch container. See log for details.") + raise LoaderNotFoundError( + "Can't switch container because loader '{}' was not found." + .format(container.get("loader")) + ) if not hasattr(loader_plugin, "switch"): # Backwards compatibility (classes without switch support # might be better to just have "switch" raise NotImplementedError # on the base class of Loader\ - raise RuntimeError("Loader '{}' does not support 'switch'".format( - loader_plugin.label - )) + raise LoaderSwitchNotImplementedError( + "Loader {} does not support 'switch'".format(loader_plugin.label) + ) # Get the new representation to switch to project_name = legacy_io.active_project() @@ -520,7 +539,11 @@ def switch_container(container, representation, loader_plugin=None): new_context = get_representation_context(new_representation) if not is_compatible_loader(loader_plugin, new_context): - raise AssertionError("Must be compatible Loader") + raise IncompatibleLoaderError( + "Loader {} is incompatible with {}".format( + loader_plugin.__name__, new_context["subset"]["name"] + ) + ) loader = loader_plugin(new_context) diff --git a/openpype/tools/sceneinventory/switch_dialog.py b/openpype/tools/sceneinventory/switch_dialog.py index 4aaad38bbc..ce2272df57 100644 --- a/openpype/tools/sceneinventory/switch_dialog.py +++ b/openpype/tools/sceneinventory/switch_dialog.py @@ -19,6 +19,9 @@ from openpype.pipeline.load import ( switch_container, get_repres_contexts, loaders_from_repre_context, + LoaderSwitchNotImplementedError, + IncompatibleLoaderError, + LoaderNotFoundError ) from .widgets import ( @@ -1298,19 +1301,28 @@ class SwitchAssetDialog(QtWidgets.QDialog): else: repre_doc = repres_by_name[container_repre_name] + error = None try: switch_container(container, repre_doc, loader) + except ( + LoaderSwitchNotImplementedError, + IncompatibleLoaderError, + LoaderNotFoundError, + ) as exc: + error = str(exc) except Exception: - msg = ( + error = ( + "Switch asset failed. " + "Search console log for more details." + ) + if error is not None: + log.warning(( "Couldn't switch asset." "See traceback for more information." - ) - log.warning(msg, exc_info=True) + ), exc_info=True) dialog = QtWidgets.QMessageBox(self) dialog.setWindowTitle("Switch asset failed") - dialog.setText( - "Switch asset failed. Search console log for more details" - ) + dialog.setText(error) dialog.exec_() self.switched.emit() From 09f0d183d8915efe72ad00056eee4955ced4cd09 Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov Date: Mon, 3 Jul 2023 15:40:16 +0300 Subject: [PATCH 061/107] hound remove whitespace --- openpype/plugins/load/delivery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/load/delivery.py b/openpype/plugins/load/delivery.py index 90e734973b..4bd4f6e9cf 100644 --- a/openpype/plugins/load/delivery.py +++ b/openpype/plugins/load/delivery.py @@ -155,7 +155,7 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): self._update_selected_label() self._update_template_value() - + btn_delivery.clicked.connect(self.deliver) dropdown.currentIndexChanged.connect(self._update_template_value) From f2315c6fd812308a605a9aba21d00473a8bb8c79 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Mon, 3 Jul 2023 16:40:35 +0300 Subject: [PATCH 062/107] add geometry check --- .../plugins/publish/validate_abc_primitive_to_detail.py | 8 ++++++++ .../plugins/publish/validate_primitive_hierarchy_paths.py | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/openpype/hosts/houdini/plugins/publish/validate_abc_primitive_to_detail.py b/openpype/hosts/houdini/plugins/publish/validate_abc_primitive_to_detail.py index 86e92a052f..bef8db45a4 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_abc_primitive_to_detail.py +++ b/openpype/hosts/houdini/plugins/publish/validate_abc_primitive_to_detail.py @@ -73,6 +73,14 @@ class ValidateAbcPrimitiveToDetail(pyblish.api.InstancePlugin): cls.log.debug("Checking Primitive to Detail pattern: %s" % pattern) cls.log.debug("Checking with path attribute: %s" % path_attr) + if not hasattr(output_node, "geometry"): + # In the case someone has explicitly set an Object + # node instead of a SOP node in Geometry context + # then for now we ignore - this allows us to also + # export object transforms. + cls.log.warning("No geometry output node found, skipping check..") + return + # Check if the primitive attribute exists frame = instance.data.get("frameStart", 0) geo = output_node.geometryAtFrame(frame) diff --git a/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py b/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py index d3a4c0cfbf..cd5e724ab3 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py +++ b/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py @@ -60,6 +60,14 @@ class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin): cls.log.debug("Checking for attribute: %s" % path_attr) + if not hasattr(output_node, "geometry"): + # In the case someone has explicitly set an Object + # node instead of a SOP node in Geometry context + # then for now we ignore - this allows us to also + # export object transforms. + cls.log.warning("No geometry output node found, skipping check..") + return + # Check if the primitive attribute exists frame = instance.data.get("frameStart", 0) geo = output_node.geometryAtFrame(frame) From a0fb1d49cad2da140e3b3ff9a791b37bdabaa199 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Mon, 3 Jul 2023 17:36:48 +0300 Subject: [PATCH 063/107] 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 744095eb161939dec966f6e52c215b72c775a521 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 3 Jul 2023 16:39:40 +0200 Subject: [PATCH 064/107] maya: allign default settings to distributed aces 1.2 config --- openpype/settings/defaults/project_settings/maya.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 19c3da13e6..e3fc5f0723 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -421,9 +421,9 @@ }, "workfile": { "enabled": false, - "renderSpace": "ACEScg", - "displayName": "sRGB", - "viewName": "ACES 1.0 SDR-video" + "renderSpace": "ACES - ACEScg", + "displayName": "ACES", + "viewName": "sRGB" }, "colorManagementPreference_v2": { "enabled": true, From 39385995785f353db4c1d9521760cae46c637035 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Mon, 3 Jul 2023 18:07:34 +0300 Subject: [PATCH 065/107] add geometry check --- .../hosts/houdini/plugins/publish/validate_vdb_output_node.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py index 674782179c..ee46b746a2 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py +++ b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py @@ -55,6 +55,8 @@ def get_geometry_at_frame(sop_node, frame, force=True): """Return geometry at frame but force a cooked value.""" with update_mode_context(hou.updateMode.AutoUpdate): sop_node.cook(force=force, frame_range=(frame, frame)) + if not hasattr(sop_node, "geometry"): + return return sop_node.geometryAtFrame(frame) From 9ccbaed576bb93b076c8d05664cb2a6bf3e289b9 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 3 Jul 2023 23:12:59 +0800 Subject: [PATCH 066/107] 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 067/107] 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 b6d4f1b142bdc73aeb6d6f402b5897d06b7a31f7 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Tue, 4 Jul 2023 10:53:28 +0300 Subject: [PATCH 068/107] move check outside with --- .../hosts/houdini/plugins/publish/validate_vdb_output_node.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py index ee46b746a2..b51e1007f0 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py +++ b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py @@ -53,10 +53,10 @@ def update_mode_context(mode): def get_geometry_at_frame(sop_node, frame, force=True): """Return geometry at frame but force a cooked value.""" + if not hasattr(sop_node, "geometry"): + return with update_mode_context(hou.updateMode.AutoUpdate): sop_node.cook(force=force, frame_range=(frame, frame)) - if not hasattr(sop_node, "geometry"): - return return sop_node.geometryAtFrame(frame) From 2362c4114e227604974920216997bfc35fe4cfd4 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 4 Jul 2023 17:44:58 +0800 Subject: [PATCH 069/107] 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 070/107] 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 071/107] 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 072/107] 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 073/107] 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 074/107] 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 075/107] 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 076/107] 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 cf8f7aa9ea8af4e1d6118f19e90012624bdf930a Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 5 Jul 2023 03:31:41 +0000 Subject: [PATCH 077/107] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index bc3cb93882..4a6131a26a 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.12-nightly.2" +__version__ = "3.15.12-nightly.3" From ae05aba0891e7107335b66309ce825eeae8af675 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 5 Jul 2023 03:32:31 +0000 Subject: [PATCH 078/107] 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 079/107] 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 080/107] 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 081/107] 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 082/107] 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 083/107] 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 084/107] 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 085/107] 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 086/107] 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 087/107] 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 088/107] 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 708819f8b13980ebacea6468f15e6b4e2cf9051c Mon Sep 17 00:00:00 2001 From: Pype Club Date: Wed, 5 Jul 2023 16:50:58 +0200 Subject: [PATCH 089/107] Refactor - renamed replace_published_scene Former name pointed to replacing of whole file. This function is only about using path to published workfile instead of work workfile. --- openpype/modules/deadline/abstract_submit_deadline.py | 5 +++-- openpype/pipeline/publish/lib.py | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/openpype/modules/deadline/abstract_submit_deadline.py b/openpype/modules/deadline/abstract_submit_deadline.py index 6ff9afbc42..d9d250fe9e 100644 --- a/openpype/modules/deadline/abstract_submit_deadline.py +++ b/openpype/modules/deadline/abstract_submit_deadline.py @@ -23,7 +23,7 @@ from openpype.pipeline.publish import ( OpenPypePyblishPluginMixin ) from openpype.pipeline.publish.lib import ( - replace_published_scene + replace_with_published_scene_path ) JSONDecodeError = getattr(json.decoder, "JSONDecodeError", ValueError) @@ -528,7 +528,8 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin, published. """ - return replace_published_scene(self._instance, replace_in_path=True) + return replace_with_published_scene_path( + self._instance, replace_in_path=replace_in_path) def assemble_payload( self, job_info=None, plugin_info=None, aux_files=None): diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index a03303bc39..5d2a88fa3b 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -882,10 +882,11 @@ def get_published_workfile_instance(context): return i -def replace_published_scene(instance, replace_in_path=True): - """Switch work scene for published scene. +def replace_with_published_scene_path(instance, replace_in_path=True): + """Switch work scene path for published scene. If rendering/exporting from published scenes is enabled, this will replace paths from working scene to published scene. + This only works if publish contains workfile instance! Args: instance (pyblish.api.Instance): Pyblish instance. replace_in_path (bool): if True, it will try to find From 26cd4f0029473aa361bfe0b46139a1e0b0f30109 Mon Sep 17 00:00:00 2001 From: Pype Club Date: Wed, 5 Jul 2023 16:51:51 +0200 Subject: [PATCH 090/107] Hound --- openpype/pipeline/farm/pyblish_functions.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/pipeline/farm/pyblish_functions.py b/openpype/pipeline/farm/pyblish_functions.py index 0ace02edb9..afd2ef3eeb 100644 --- a/openpype/pipeline/farm/pyblish_functions.py +++ b/openpype/pipeline/farm/pyblish_functions.py @@ -822,8 +822,6 @@ def attach_instances_to_subset(attach_to, instances): list: List of attached instances. """ - # - new_instances = [] for attach_instance in attach_to: for i in instances: 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 091/107] 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 66832f3c323fb7ab7e24e9a669ef5719286ffea6 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 5 Jul 2023 18:21:40 +0300 Subject: [PATCH 092/107] add better selection --- .../plugins/create/create_pointcache.py | 68 ++++++++++++++++--- 1 file changed, 58 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_pointcache.py b/openpype/hosts/houdini/plugins/create/create_pointcache.py index df74070fee..00ff22bdda 100644 --- a/openpype/hosts/houdini/plugins/create/create_pointcache.py +++ b/openpype/hosts/houdini/plugins/create/create_pointcache.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- """Creator plugin for creating pointcache alembics.""" from openpype.hosts.houdini.api import plugin -from openpype.pipeline import CreatedInstance import hou @@ -14,15 +13,13 @@ class CreatePointCache(plugin.HoudiniCreator): icon = "gears" def create(self, subset_name, instance_data, pre_create_data): - import hou - instance_data.pop("active", None) instance_data.update({"node_type": "alembic"}) instance = super(CreatePointCache, self).create( subset_name, instance_data, - pre_create_data) # type: CreatedInstance + pre_create_data) instance_node = hou.node(instance.get("instance_node")) parms = { @@ -37,13 +34,50 @@ class CreatePointCache(plugin.HoudiniCreator): } if self.selected_nodes: - parms["sop_path"] = self.selected_nodes[0].path() + selected_node = self.selected_nodes[0] - # try to find output node - for child in self.selected_nodes[0].children(): - if child.type().name() == "output": - parms["sop_path"] = child.path() - break + # Although Houdini allows ObjNode path on `sop_path`rop node + # However, it's preferred to set SopNode path explicitly + # These checks prevent using user selecting + + # Allow sop level paths (e.g. /obj/geo1/box1) + # but do not allow other sop level paths when + # the parent type is not 'geo' like + # Cameras, Dopnet nodes(sop solver) + if isinstance(selected_node, hou.SopNode) and \ + selected_node.parent().type().name() == 'geo': + parms["sop_path"] = selected_node.path() + self.log.debug( + "Valid SopNode selection, 'SOP Path' in ROP will be set to '%s'." + % selected_node.path() + ) + + # Allow object level paths to Geometry nodes (e.g. /obj/geo1) + # but do not allow other object level nodes types like cameras, etc. + elif isinstance(selected_node, hou.ObjNode) and \ + selected_node.type().name() == 'geo': + + # get the output node with the minimum + # 'outputidx' or the node with display flag + sop_path = self.get_obj_output(selected_node) or \ + selected_node.displayNode() + + if sop_path: + parms["sop_path"] = sop_path.path() + self.log.debug( + "Valid ObjNode selection, 'SOP Path' in ROP will be set to " + "the child path '%s'." + % sop_path.path() + ) + + if not parms.get("sop_path", None): + self.log.debug( + "Selection isn't valid.'SOP Path' in ROP will be empty." + ) + else: + self.log.debug( + "No Selection.'SOP Path' in ROP will be empty." + ) instance_node.setParms(parms) instance_node.parm("trange").set(1) @@ -57,3 +91,17 @@ class CreatePointCache(plugin.HoudiniCreator): hou.ropNodeTypeCategory(), hou.sopNodeTypeCategory() ] + + def get_obj_output(self, obj_node): + """Find output node with the smallest 'outputidx'.""" + + outputs = dict() + + for sop_node in obj_node.children(): + if sop_node.type().name() == 'output' : + outputs.update({sop_node : sop_node.parm('outputidx').eval()}) + + if outputs: + return min(outputs, key = outputs.get) + else: + return From e710f5f70e491a1c744e21ac5e6693f772c56b43 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 5 Jul 2023 18:48:12 +0300 Subject: [PATCH 093/107] add subnet to allowed --- openpype/hosts/houdini/plugins/create/create_pointcache.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_pointcache.py b/openpype/hosts/houdini/plugins/create/create_pointcache.py index 00ff22bdda..931b8561a8 100644 --- a/openpype/hosts/houdini/plugins/create/create_pointcache.py +++ b/openpype/hosts/houdini/plugins/create/create_pointcache.py @@ -45,7 +45,7 @@ class CreatePointCache(plugin.HoudiniCreator): # the parent type is not 'geo' like # Cameras, Dopnet nodes(sop solver) if isinstance(selected_node, hou.SopNode) and \ - selected_node.parent().type().name() == 'geo': + selected_node.parent().type().name() in ['geo', 'subnet']: parms["sop_path"] = selected_node.path() self.log.debug( "Valid SopNode selection, 'SOP Path' in ROP will be set to '%s'." @@ -55,7 +55,7 @@ class CreatePointCache(plugin.HoudiniCreator): # Allow object level paths to Geometry nodes (e.g. /obj/geo1) # but do not allow other object level nodes types like cameras, etc. elif isinstance(selected_node, hou.ObjNode) and \ - selected_node.type().name() == 'geo': + selected_node.type().name() in ['geo']: # get the output node with the minimum # 'outputidx' or the node with display flag From 5009df0f2893827b739032094ab7af2dad8e7b22 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 5 Jul 2023 20:02:33 +0300 Subject: [PATCH 094/107] up date get_obj_output --- .../plugins/create/create_pointcache.py | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_pointcache.py b/openpype/hosts/houdini/plugins/create/create_pointcache.py index 931b8561a8..d36c6dfb3f 100644 --- a/openpype/hosts/houdini/plugins/create/create_pointcache.py +++ b/openpype/hosts/houdini/plugins/create/create_pointcache.py @@ -36,9 +36,8 @@ class CreatePointCache(plugin.HoudiniCreator): if self.selected_nodes: selected_node = self.selected_nodes[0] - # Although Houdini allows ObjNode path on `sop_path`rop node - # However, it's preferred to set SopNode path explicitly - # These checks prevent using user selecting + # Although Houdini allows ObjNode path on `sop_path` for the + # the ROP node we prefer it set to the SopNode path explicitly # Allow sop level paths (e.g. /obj/geo1/box1) # but do not allow other sop level paths when @@ -72,11 +71,11 @@ class CreatePointCache(plugin.HoudiniCreator): if not parms.get("sop_path", None): self.log.debug( - "Selection isn't valid.'SOP Path' in ROP will be empty." + "Selection isn't valid. 'SOP Path' in ROP will be empty." ) else: self.log.debug( - "No Selection.'SOP Path' in ROP will be empty." + "No Selection. 'SOP Path' in ROP will be empty." ) instance_node.setParms(parms) @@ -95,13 +94,18 @@ class CreatePointCache(plugin.HoudiniCreator): def get_obj_output(self, obj_node): """Find output node with the smallest 'outputidx'.""" - outputs = dict() + outputs = obj_node.subnetOutputs() - for sop_node in obj_node.children(): - if sop_node.type().name() == 'output' : - outputs.update({sop_node : sop_node.parm('outputidx').eval()}) - - if outputs: - return min(outputs, key = outputs.get) - else: + # if obj_node is empty + if not outputs: return + + # if obj_node has one output child whether its + # sop output node or a node with the render flag + elif len(outputs)==1: + return outputs[0] + + # if there are more than one, then it have multiple ouput nodes + # return the one with the minimum 'outputidx' + else: + return (min(outputs, key=lambda node : node.parm('outputidx').eval())) From 10dcc4440cd0218c899c54e70581a4814ce54e6e Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 6 Jul 2023 11:24:39 +0300 Subject: [PATCH 095/107] fix lint problems --- .../houdini/plugins/create/create_pointcache.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_pointcache.py b/openpype/hosts/houdini/plugins/create/create_pointcache.py index d36c6dfb3f..b06fb64603 100644 --- a/openpype/hosts/houdini/plugins/create/create_pointcache.py +++ b/openpype/hosts/houdini/plugins/create/create_pointcache.py @@ -44,7 +44,7 @@ class CreatePointCache(plugin.HoudiniCreator): # the parent type is not 'geo' like # Cameras, Dopnet nodes(sop solver) if isinstance(selected_node, hou.SopNode) and \ - selected_node.parent().type().name() in ['geo', 'subnet']: + selected_node.parent().type().name() in ["geo", "subnet"]: parms["sop_path"] = selected_node.path() self.log.debug( "Valid SopNode selection, 'SOP Path' in ROP will be set to '%s'." @@ -54,7 +54,7 @@ class CreatePointCache(plugin.HoudiniCreator): # Allow object level paths to Geometry nodes (e.g. /obj/geo1) # but do not allow other object level nodes types like cameras, etc. elif isinstance(selected_node, hou.ObjNode) and \ - selected_node.type().name() in ['geo']: + selected_node.type().name() in ["geo"]: # get the output node with the minimum # 'outputidx' or the node with display flag @@ -71,7 +71,7 @@ class CreatePointCache(plugin.HoudiniCreator): if not parms.get("sop_path", None): self.log.debug( - "Selection isn't valid. 'SOP Path' in ROP will be empty." + "Selection isn't valid. 'SOP Path' in ROP will be empty." ) else: self.log.debug( @@ -102,10 +102,11 @@ class CreatePointCache(plugin.HoudiniCreator): # if obj_node has one output child whether its # sop output node or a node with the render flag - elif len(outputs)==1: + elif len(outputs) == 1: return outputs[0] # if there are more than one, then it have multiple ouput nodes # return the one with the minimum 'outputidx' - else: - return (min(outputs, key=lambda node : node.parm('outputidx').eval())) + else : + return min(outputs, + key=lambda node : node.parm('outputidx').eval()) From fa57cc4bdc14f6b01306a940b20b1e3907fa29df Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 6 Jul 2023 11:27:29 +0300 Subject: [PATCH 096/107] fix lint problems --- openpype/hosts/houdini/plugins/create/create_pointcache.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_pointcache.py b/openpype/hosts/houdini/plugins/create/create_pointcache.py index b06fb64603..98d46d0eef 100644 --- a/openpype/hosts/houdini/plugins/create/create_pointcache.py +++ b/openpype/hosts/houdini/plugins/create/create_pointcache.py @@ -107,6 +107,6 @@ class CreatePointCache(plugin.HoudiniCreator): # if there are more than one, then it have multiple ouput nodes # return the one with the minimum 'outputidx' - else : + else: return min(outputs, - key=lambda node : node.parm('outputidx').eval()) + key=lambda node : node.parm('outputidx').eval()) From fdf35e115b1373a9c49be9e62ae506445894f902 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 6 Jul 2023 12:39:13 +0300 Subject: [PATCH 097/107] fix lint problems --- openpype/hosts/houdini/plugins/create/create_pointcache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/create/create_pointcache.py b/openpype/hosts/houdini/plugins/create/create_pointcache.py index 98d46d0eef..18ca5240ad 100644 --- a/openpype/hosts/houdini/plugins/create/create_pointcache.py +++ b/openpype/hosts/houdini/plugins/create/create_pointcache.py @@ -109,4 +109,4 @@ class CreatePointCache(plugin.HoudiniCreator): # return the one with the minimum 'outputidx' else: return min(outputs, - key=lambda node : node.parm('outputidx').eval()) + key=lambda node : node.parm('outputidx').eval()) From ac517edf5545b0ffbdd7e47f22ee8a8aea84ec80 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 6 Jul 2023 13:35:13 +0300 Subject: [PATCH 098/107] resolve conversation --- openpype/hosts/houdini/plugins/create/create_pointcache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/create/create_pointcache.py b/openpype/hosts/houdini/plugins/create/create_pointcache.py index 18ca5240ad..cf5f9b2edb 100644 --- a/openpype/hosts/houdini/plugins/create/create_pointcache.py +++ b/openpype/hosts/houdini/plugins/create/create_pointcache.py @@ -109,4 +109,4 @@ class CreatePointCache(plugin.HoudiniCreator): # return the one with the minimum 'outputidx' else: return min(outputs, - key=lambda node : node.parm('outputidx').eval()) + key=lambda node : node.evalParm('outputidx')) From 44571c1f062ee2ed607378f560332bae824b2225 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 6 Jul 2023 13:42:50 +0300 Subject: [PATCH 099/107] delete unnecessary function call --- openpype/hosts/houdini/plugins/create/create_pointcache.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_pointcache.py b/openpype/hosts/houdini/plugins/create/create_pointcache.py index cf5f9b2edb..8914269f45 100644 --- a/openpype/hosts/houdini/plugins/create/create_pointcache.py +++ b/openpype/hosts/houdini/plugins/create/create_pointcache.py @@ -58,8 +58,7 @@ class CreatePointCache(plugin.HoudiniCreator): # get the output node with the minimum # 'outputidx' or the node with display flag - sop_path = self.get_obj_output(selected_node) or \ - selected_node.displayNode() + sop_path = self.get_obj_output(selected_node) if sop_path: parms["sop_path"] = sop_path.path() From d58ef791f81130a9552811ff4e95da5abe9be87a Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 6 Jul 2023 13:47:52 +0300 Subject: [PATCH 100/107] make hound happy --- openpype/hosts/houdini/plugins/create/create_pointcache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/create/create_pointcache.py b/openpype/hosts/houdini/plugins/create/create_pointcache.py index 8914269f45..4eadef86f7 100644 --- a/openpype/hosts/houdini/plugins/create/create_pointcache.py +++ b/openpype/hosts/houdini/plugins/create/create_pointcache.py @@ -108,4 +108,4 @@ class CreatePointCache(plugin.HoudiniCreator): # return the one with the minimum 'outputidx' else: return min(outputs, - key=lambda node : node.evalParm('outputidx')) + key=lambda node: node.evalParm('outputidx')) From 6b1707d51fc633db190300f46f5c72a3c1395590 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 6 Jul 2023 17:37:21 +0300 Subject: [PATCH 101/107] make it less restrictive --- openpype/hosts/houdini/plugins/create/create_pointcache.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_pointcache.py b/openpype/hosts/houdini/plugins/create/create_pointcache.py index 4eadef86f7..554d5f2016 100644 --- a/openpype/hosts/houdini/plugins/create/create_pointcache.py +++ b/openpype/hosts/houdini/plugins/create/create_pointcache.py @@ -40,11 +40,7 @@ class CreatePointCache(plugin.HoudiniCreator): # the ROP node we prefer it set to the SopNode path explicitly # Allow sop level paths (e.g. /obj/geo1/box1) - # but do not allow other sop level paths when - # the parent type is not 'geo' like - # Cameras, Dopnet nodes(sop solver) - if isinstance(selected_node, hou.SopNode) and \ - selected_node.parent().type().name() in ["geo", "subnet"]: + if isinstance(selected_node, hou.SopNode): parms["sop_path"] = selected_node.path() self.log.debug( "Valid SopNode selection, 'SOP Path' in ROP will be set to '%s'." From 4d87046f6a4373fa2587de7c9b2537258101461c Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 6 Jul 2023 16:42:05 +0100 Subject: [PATCH 102/107] 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 103/107] 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 104/107] 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() From 0611163c1f3fdcda3ffaa2dca3caf8389a98ffc8 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 8 Jul 2023 03:31:49 +0000 Subject: [PATCH 105/107] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index 4a6131a26a..cdd546c4a0 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.12-nightly.3" +__version__ = "3.15.12-nightly.4" From 680ea6d0d71bbee13d3ff85bc0ff6c444501d60d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 8 Jul 2023 03:32:33 +0000 Subject: [PATCH 106/107] 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 9fcb69e2e9..1280e6a6e5 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.4 - 3.15.12-nightly.3 - 3.15.12-nightly.2 - 3.15.12-nightly.1 @@ -134,7 +135,6 @@ body: - 3.14.5-nightly.1 - 3.14.4 - 3.14.4-nightly.4 - - 3.14.4-nightly.3 validations: required: true - type: dropdown From 252e1f03076860778e8b4a5c8a3049f17e2efa0b Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 10 Jul 2023 10:01:38 +0100 Subject: [PATCH 107/107] Move Qt imports away from module init --- openpype/hosts/unreal/addon.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/addon.py b/openpype/hosts/unreal/addon.py index ed23950b35..b5c978d98f 100644 --- a/openpype/hosts/unreal/addon.py +++ b/openpype/hosts/unreal/addon.py @@ -1,7 +1,6 @@ import os import re from openpype.modules import IHostAddon, OpenPypeModule -from openpype.widgets.message_window import Window UNREAL_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -21,6 +20,8 @@ class UnrealAddon(OpenPypeModule, IHostAddon): from .lib import get_compatible_integration + from openpype.widgets.message_window import Window + pattern = re.compile(r'^\d+-\d+$') if not pattern.match(app.name):