From a6e482484eb8c633caf26e93ebb0774ffe98eac7 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 26 May 2023 11:30:40 +0100 Subject: [PATCH 001/327] Allow for knob values to be validated against multiple values. --- .../plugins/publish/validate_write_nodes.py | 44 ++++++++++++------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py b/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py index aeecea655f..2a925fbeff 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py +++ b/openpype/hosts/nuke/plugins/publish/validate_write_nodes.py @@ -1,3 +1,5 @@ +from collections import defaultdict + import pyblish.api from openpype.pipeline.publish import get_errored_instances_from_context from openpype.hosts.nuke.api.lib import ( @@ -87,6 +89,11 @@ class ValidateNukeWriteNode( correct_data )) + # Collect key values of same type in a list. + values_by_name = defaultdict(list) + for knob_data in correct_data["knobs"]: + values_by_name[knob_data["name"]].append(knob_data["value"]) + for knob_data in correct_data["knobs"]: knob_type = knob_data["type"] self.log.debug("__ knob_type: {}".format( @@ -105,28 +112,33 @@ class ValidateNukeWriteNode( ) key = knob_data["name"] - value = knob_data["value"] + values = values_by_name[key] node_value = write_node[key].value() # fix type differences - if type(node_value) in (int, float): - try: - if isinstance(value, list): - value = color_gui_to_int(value) - else: - value = float(value) - node_value = float(node_value) - except ValueError: - value = str(value) - else: - value = str(value) - node_value = str(node_value) + fixed_values = [] + for value in values: + if type(node_value) in (int, float): + try: - self.log.debug("__ key: {} | value: {}".format( - key, value + if isinstance(value, list): + value = color_gui_to_int(value) + else: + value = float(value) + node_value = float(node_value) + except ValueError: + value = str(value) + else: + value = str(value) + node_value = str(node_value) + + fixed_values.append(value) + + self.log.debug("__ key: {} | values: {}".format( + key, fixed_values )) if ( - node_value != value + node_value not in fixed_values and key != "file" and key != "tile_color" ): From a216087608ea12c2920b6220102f0c77a8312702 Mon Sep 17 00:00:00 2001 From: Fabia Serra Arrizabalaga Date: Mon, 26 Jun 2023 10:16:51 -0500 Subject: [PATCH 002/327] Fix typos --- openpype/hosts/nuke/plugins/publish/extract_camera.py | 4 ++-- openpype/hosts/nuke/plugins/publish/extract_model.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_camera.py b/openpype/hosts/nuke/plugins/publish/extract_camera.py index 4286f71e83..33df6258ae 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_camera.py +++ b/openpype/hosts/nuke/plugins/publish/extract_camera.py @@ -11,9 +11,9 @@ from openpype.hosts.nuke.api.lib import maintained_selection class ExtractCamera(publish.Extractor): - """ 3D camera exctractor + """ 3D camera extractor """ - label = 'Exctract Camera' + label = 'Extract Camera' order = pyblish.api.ExtractorOrder families = ["camera"] hosts = ["nuke"] diff --git a/openpype/hosts/nuke/plugins/publish/extract_model.py b/openpype/hosts/nuke/plugins/publish/extract_model.py index 814d404137..00462f8035 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_model.py +++ b/openpype/hosts/nuke/plugins/publish/extract_model.py @@ -11,9 +11,9 @@ from openpype.hosts.nuke.api.lib import ( class ExtractModel(publish.Extractor): - """ 3D model exctractor + """ 3D model extractor """ - label = 'Exctract Model' + label = 'Extract Model' order = pyblish.api.ExtractorOrder families = ["model"] hosts = ["nuke"] From b4ae58634d3697c7ccf72ba58fd93d5afa3af24e Mon Sep 17 00:00:00 2001 From: Fabia Serra Arrizabalaga Date: Mon, 26 Jun 2023 10:19:02 -0500 Subject: [PATCH 003/327] Nuke: Add support to submit existing frames to the farm --- openpype/hosts/nuke/api/plugin.py | 3 ++- .../nuke/plugins/publish/collect_writes.py | 21 ++++++++++++++++++- .../plugins/publish/submit_nuke_deadline.py | 2 +- .../plugins/publish/submit_publish_job.py | 16 +++++++++----- .../validate_expected_and_rendered_files.py | 4 ++++ 5 files changed, 38 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index 7035da2bb5..b82ed0a1e3 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -311,6 +311,7 @@ class NukeWriteCreator(NukeCreator): } if ("farm_rendering" in self.instance_attributes): rendering_targets["farm"] = "Farm rendering" + rendering_targets["farm_frames"] = "Existing frames farm rendering" return EnumDef( "render_target", @@ -669,7 +670,7 @@ class ExporterReviewLut(ExporterReview): self.ext = ext or "cube" self.cube_size = cube_size or 32 self.lut_size = lut_size or 1024 - self.lut_style = lut_style or "linear" + self.lut_style = lut_style or "scene_linear" # set frame start / end and file name to self self.get_file_info() diff --git a/openpype/hosts/nuke/plugins/publish/collect_writes.py b/openpype/hosts/nuke/plugins/publish/collect_writes.py index 2d1caacdc3..0b6fe392d8 100644 --- a/openpype/hosts/nuke/plugins/publish/collect_writes.py +++ b/openpype/hosts/nuke/plugins/publish/collect_writes.py @@ -29,6 +29,9 @@ class CollectNukeWrites(pyblish.api.InstancePlugin, instance.data["families"].append( "{}.{}".format(family, render_target) ) + self.log.debug("Appending render target to families: {}.{}".format( + family, render_target) + ) if instance.data.get("review"): instance.data["families"].append("review") @@ -73,7 +76,7 @@ class CollectNukeWrites(pyblish.api.InstancePlugin, self.log.debug('output dir: {}'.format(output_dir)) - if render_target == "frames": + if render_target in ["frames", "farm_frames"]: representation = { 'name': ext, 'ext': ext, @@ -142,6 +145,22 @@ class CollectNukeWrites(pyblish.api.InstancePlugin, instance.data["representations"].append(representation) self.log.info("Publishing rendered frames ...") + if render_target == "farm_frames": + # Farm rendering + instance.data["toBeRenderedOn"] = "deadline" + instance.data["transfer"] = False + instance.data["farm"] = True # to skip integrate + self.log.info("Farm rendering ON ...") + + self.log.info( + "Adding collected files %s to expectedFiles instance.data", + collected_frames + ) + if "expectedFiles" not in instance.data: + instance.data["expectedFiles"] = list() + for source_file in collected_frames: + instance.data["expectedFiles"].append(os.path.join(output_dir, source_file)) + elif render_target == "farm": farm_keys = ["farm_chunk", "farm_priority", "farm_concurrency"] for key in farm_keys: diff --git a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py index 4900231783..34e335e442 100644 --- a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py @@ -32,7 +32,7 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin, label = "Submit Nuke to Deadline" order = pyblish.api.IntegratorOrder + 0.1 hosts = ["nuke"] - families = ["render", "prerender"] + families = ["render.farm", "prerender.farm"] optional = True targets = ["local"] diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 69e9fb6449..677d322a19 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -17,6 +17,7 @@ from openpype.client import ( from openpype.pipeline import ( get_representation_path, legacy_io, + publish, ) from openpype.tests.lib import is_in_tests from openpype.pipeline.farm.patterning import match_aov_pattern @@ -123,7 +124,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, hosts = ["fusion", "max", "maya", "nuke", "houdini", "celaction", "aftereffects", "harmony"] - families = ["render.farm", "prerender.farm", + families = ["render.farm", "render.farm_frames", + "prerender.farm", "prerender.farm_frames", "renderlayer", "imagesequence", "vrayscene", "maxrender", "arnold_rop", "mantra_rop", @@ -334,7 +336,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, for assembly_id in instance.data["bakingSubmissionJobs"]: payload["JobInfo"]["JobDependency{}".format(job_index)] = assembly_id # noqa: E501 job_index += 1 - else: + elif job.get("_id"): payload["JobInfo"]["JobDependency0"] = job["_id"] if instance.data.get("suspend_publish"): @@ -870,6 +872,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, "multipartExr": data.get("multipartExr", False), "jobBatchName": data.get("jobBatchName", ""), "useSequenceForReview": data.get("useSequenceForReview", True), + "colorspace": data.get("colorspace"), # map inputVersions `ObjectId` -> `str` so json supports it "inputVersions": list(map(str, data.get("inputVersions", []))), "colorspace": instance.data.get("colorspace") @@ -883,6 +886,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, # transfer specific families from original instance to new render for item in self.families_transfer: if item in instance.data.get("families", []): + self.log.debug("Transfering '%s' family to instance.", item) instance_skeleton_data["families"] += [item] # transfer specific properties from original instance based on @@ -890,6 +894,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, for key, values in self.instance_transfer.items(): if key in instance.data.get("families", []): for v in values: + self.log.debug("Transfering '%s' property to instance.", v) instance_skeleton_data[v] = instance.data.get(v) # look into instance data if representations are not having any @@ -912,7 +917,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, repre["stagingDir"] = staging_dir if "publish_on_farm" in repre.get("tags"): - # create representations attribute of not there + # create representations attribute if not there if "representations" not in instance_skeleton_data.keys(): instance_skeleton_data["representations"] = [] @@ -1095,6 +1100,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, "FTRACK_SERVER": os.environ.get("FTRACK_SERVER"), } + deadline_publish_job_id = None if submission_type == "deadline": # get default deadline webservice url from deadline module self.deadline_url = instance.context.data["defaultDeadline"] @@ -1118,7 +1124,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, "fps": context.data.get("fps", None), "source": source, "user": context.data["user"], - "version": context.data["version"], # this is workfile version + "version": context.data.get("version"), # this is workfile version "intent": context.data.get("intent"), "comment": context.data.get("comment"), "job": render_job or None, @@ -1151,7 +1157,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, json.dump(publish_job, f, indent=4, sort_keys=True) def _extend_frames(self, asset, subset, start, end): - """Get latest version of asset nad update frame range. + """Get latest version of asset and update frame range. Based on minimum and maximuma values. diff --git a/openpype/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py b/openpype/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py index ff4be677e7..6fbaa0d7b5 100644 --- a/openpype/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py +++ b/openpype/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py @@ -21,6 +21,10 @@ class ValidateExpectedFiles(pyblish.api.InstancePlugin): def process(self, instance): self.instance = instance + # TODO: Find a better way to check whether a job has been submitted with + # existing frames + if not instance.data["render_job_id"]: + return frame_list = self._get_frame_list(instance.data["render_job_id"]) for repre in instance.data["representations"]: From 12ea12c2dedb232bae3b134142e124bc45605837 Mon Sep 17 00:00:00 2001 From: Fabia Serra Arrizabalaga Date: Mon, 26 Jun 2023 10:46:21 -0500 Subject: [PATCH 004/327] Shush the hound --- openpype/hosts/nuke/plugins/publish/collect_writes.py | 6 ++++-- .../modules/deadline/plugins/publish/submit_publish_job.py | 3 --- .../plugins/publish/validate_expected_and_rendered_files.py | 4 ++-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/collect_writes.py b/openpype/hosts/nuke/plugins/publish/collect_writes.py index 0b6fe392d8..83ad90135f 100644 --- a/openpype/hosts/nuke/plugins/publish/collect_writes.py +++ b/openpype/hosts/nuke/plugins/publish/collect_writes.py @@ -149,7 +149,7 @@ class CollectNukeWrites(pyblish.api.InstancePlugin, # Farm rendering instance.data["toBeRenderedOn"] = "deadline" instance.data["transfer"] = False - instance.data["farm"] = True # to skip integrate + instance.data["farm"] = True # to skip integrate self.log.info("Farm rendering ON ...") self.log.info( @@ -159,7 +159,9 @@ class CollectNukeWrites(pyblish.api.InstancePlugin, if "expectedFiles" not in instance.data: instance.data["expectedFiles"] = list() for source_file in collected_frames: - instance.data["expectedFiles"].append(os.path.join(output_dir, source_file)) + instance.data["expectedFiles"].append( + os.path.join(output_dir, source_file) + ) elif render_target == "farm": farm_keys = ["farm_chunk", "farm_priority", "farm_concurrency"] diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 677d322a19..b271f849ba 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -872,7 +872,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, "multipartExr": data.get("multipartExr", False), "jobBatchName": data.get("jobBatchName", ""), "useSequenceForReview": data.get("useSequenceForReview", True), - "colorspace": data.get("colorspace"), # map inputVersions `ObjectId` -> `str` so json supports it "inputVersions": list(map(str, data.get("inputVersions", []))), "colorspace": instance.data.get("colorspace") @@ -886,7 +885,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, # transfer specific families from original instance to new render for item in self.families_transfer: if item in instance.data.get("families", []): - self.log.debug("Transfering '%s' family to instance.", item) instance_skeleton_data["families"] += [item] # transfer specific properties from original instance based on @@ -894,7 +892,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, for key, values in self.instance_transfer.items(): if key in instance.data.get("families", []): for v in values: - self.log.debug("Transfering '%s' property to instance.", v) instance_skeleton_data[v] = instance.data.get(v) # look into instance data if representations are not having any diff --git a/openpype/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py b/openpype/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py index 6fbaa0d7b5..198db7872d 100644 --- a/openpype/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py +++ b/openpype/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py @@ -21,8 +21,8 @@ class ValidateExpectedFiles(pyblish.api.InstancePlugin): def process(self, instance): self.instance = instance - # TODO: Find a better way to check whether a job has been submitted with - # existing frames + # TODO: Find a better way to check whether a job has been submitted + # with existing frames if not instance.data["render_job_id"]: return frame_list = self._get_frame_list(instance.data["render_job_id"]) From 5c0e02f889da4792a80edb745d5e7a7608f9eca0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A0=20Serra=20Arrizabalaga?= Date: Mon, 3 Jul 2023 20:57:33 +0200 Subject: [PATCH 005/327] Reverting colorspace fallback --- openpype/hosts/nuke/api/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index b82ed0a1e3..be5230d1c1 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -670,7 +670,7 @@ class ExporterReviewLut(ExporterReview): self.ext = ext or "cube" self.cube_size = cube_size or 32 self.lut_size = lut_size or 1024 - self.lut_style = lut_style or "scene_linear" + self.lut_style = lut_style or "linear" # set frame start / end and file name to self self.get_file_info() From 494e662582011ab5e62fc5e814d48a62ef3747f0 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 13 Jul 2023 15:13:53 +0800 Subject: [PATCH 006/327] add optional viewport camera validator for validation of renderable camera --- .../publish/validate_no_max_content.py | 1 - .../publish/validate_viewport_camera.py | 43 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 openpype/hosts/max/plugins/publish/validate_viewport_camera.py 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 c6a27dace3..73e12e75c9 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,6 @@ class ValidateMaxContents(pyblish.api.InstancePlugin): order = pyblish.api.ValidatorOrder families = ["camera", "maxScene", - "maxrender", "review"] hosts = ["max"] label = "Max Scene Contents" diff --git a/openpype/hosts/max/plugins/publish/validate_viewport_camera.py b/openpype/hosts/max/plugins/publish/validate_viewport_camera.py new file mode 100644 index 0000000000..a52b7e0212 --- /dev/null +++ b/openpype/hosts/max/plugins/publish/validate_viewport_camera.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +import pyblish.api +from openpype.pipeline import ( + PublishValidationError, + OptionalPyblishPluginMixin) +from openpype.pipeline.publish import RepairAction + +from pymxs import runtime as rt + + +class ValidateViewportCamera(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): + """Validates Viewport Camera + + Check if the renderable camera in scene used as viewport + camera for rendering + """ + + order = pyblish.api.ValidatorOrder + families = ["maxrender"] + hosts = ["max"] + label = "Viewport Camera" + optional = True + actions = [RepairAction] + + def process(self, instance): + if not self.is_active(instance.data): + return + cameras_in_scene = [c for c in rt.Objects + if rt.classOf(c) in rt.Camera.Classes] + if rt.viewport.getCamera() not in cameras_in_scene: + raise PublishValidationError( + "Cameras in Scene not used as viewport camera" + ) + + @classmethod + def repair(cls, instance): + # Get all cameras in the scene + cameras_in_scene = [c for c in rt.Objects + if rt.classOf(c) in rt.Camera.Classes] + # Set the first camera as viewport camera for rendering + if cameras_in_scene: + rt.viewport.setCamera(cameras_in_scene[0]) \ No newline at end of file From ceaffbbfa92f50d2da009b3be30c5df94d015eed Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 13 Jul 2023 15:26:32 +0800 Subject: [PATCH 007/327] add log for repair action --- .../hosts/max/plugins/publish/validate_viewport_camera.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_viewport_camera.py b/openpype/hosts/max/plugins/publish/validate_viewport_camera.py index a52b7e0212..d5cf85eb69 100644 --- a/openpype/hosts/max/plugins/publish/validate_viewport_camera.py +++ b/openpype/hosts/max/plugins/publish/validate_viewport_camera.py @@ -36,8 +36,10 @@ class ValidateViewportCamera(pyblish.api.InstancePlugin, @classmethod def repair(cls, instance): # Get all cameras in the scene - cameras_in_scene = [c for c in rt.Objects + cameras_in_scene = [c.name for c in rt.Objects if rt.classOf(c) in rt.Camera.Classes] # Set the first camera as viewport camera for rendering if cameras_in_scene: - rt.viewport.setCamera(cameras_in_scene[0]) \ No newline at end of file + camera = rt.getNodeByName(cameras_in_scene[0]) + rt.viewport.setCamera(camera) + cls.log.info(f"Camera {camera} set as viewport camera") From ed7db7a7e8efcc614e48c8912d5807f42af87104 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 12 Jul 2023 21:28:27 +0200 Subject: [PATCH 008/327] deadline: adding OCIO env var to submit publish job --- .../modules/deadline/plugins/publish/submit_publish_job.py | 3 ++- 1 file changed, 2 insertions(+), 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 292fe58cca..e220d96a80 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -146,7 +146,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, "FTRACK_SERVER", "AVALON_APP_NAME", "OPENPYPE_USERNAME", - "OPENPYPE_SG_USER" + "OPENPYPE_SG_USER", + "OCIO", ] # Add OpenPype version if we are running from build. From 52d643be71d91f0409052a03f85c5b4225b99b13 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 12 Jul 2023 21:28:27 +0200 Subject: [PATCH 009/327] deadline: adding OCIO env var to submit publish job From 8497b49951802cda8c62d811ffa1d10cecf52f4f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 12 Jul 2023 21:33:45 +0200 Subject: [PATCH 010/327] adding OCIO env to host submitters - also removing OPENPYPE_VERSION deprecated code --- .../plugins/publish/submit_houdini_render_deadline.py | 2 +- .../deadline/plugins/publish/submit_max_deadline.py | 10 ++++++++-- .../deadline/plugins/publish/submit_maya_deadline.py | 3 ++- .../deadline/plugins/publish/submit_nuke_deadline.py | 1 + 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py index 254914a850..1f4770653c 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -88,7 +88,7 @@ class HoudiniSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): "AVALON_APP_NAME", "OPENPYPE_DEV", "OPENPYPE_LOG_NO_COLORS", - "OPENPYPE_VERSION" + "OCIO", ] # Add OpenPype version if we are running from build. diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index b6a30e36b7..43c89d2682 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -20,6 +20,7 @@ from openpype.hosts.max.api.lib import ( from openpype.hosts.max.api.lib_rendersettings import RenderSettings from openpype_modules.deadline import abstract_submit_deadline from openpype_modules.deadline.abstract_submit_deadline import DeadlineJobInfo +from openpype.lib import is_running_from_build @attr.s @@ -110,9 +111,14 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, "AVALON_TASK", "AVALON_APP_NAME", "OPENPYPE_DEV", - "OPENPYPE_VERSION", - "IS_TEST" + "IS_TEST", + "OCIO", ] + + # Add OpenPype version if we are running from build. + if is_running_from_build(): + keys.append("OPENPYPE_VERSION") + # Add mongo url if it's enabled if self._instance.context.data.get("deadlinePassMongoUrl"): keys.append("OPENPYPE_MONGO") diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py index a6cdcb7e71..3370be8815 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -165,7 +165,8 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): "AVALON_TASK", "AVALON_APP_NAME", "OPENPYPE_DEV" - "IS_TEST" + "IS_TEST", + "OCIO", ] # Add OpenPype version if we are running from build. diff --git a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py index 4900231783..e52ee632c5 100644 --- a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py @@ -316,6 +316,7 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin, "TOOL_ENV", "FOUNDRY_LICENSE", "OPENPYPE_SG_USER", + "OCIO", ] # Add OpenPype version if we are running from build. From 54e2687afa13b4963b86ecbc4b99786e1a57ced2 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Tue, 18 Jul 2023 10:21:21 +0300 Subject: [PATCH 011/327] create review validator --- .../publish/validate_review_colorspace.py | 143 ++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py diff --git a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py new file mode 100644 index 0000000000..18ca39234c --- /dev/null +++ b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- +import pyblish.api + +from collections import defaultdict +from openpype.pipeline import PublishValidationError + + +class ValidateReviewColorspace(pyblish.api.InstancePlugin): + """Validate Review Colorspace parameters. + + + """ + + order = pyblish.api.ValidatorOrder + 0.1 + families = ["review"] + hosts = ["houdini"] + label = "Validate Review Colorspace" + + def process(self, instance): + invalid = self.get_invalid(instance) + if invalid: + raise PublishValidationError( + ("Colorspace parameter is not valid."), + title=self.label + ) + + @classmethod + def get_invalid(cls, instance): + import hou # noqa + output_node = instance.data.get("output_node") + rop_node = hou.node(instance.data["instance_node"]) + if output_node is None: + cls.log.error( + "SOP Output node in '%s' does not exist. " + "Ensure a valid SOP output path is set." % rop_node.path() + ) + + return [rop_node.path()] + + pattern = rop_node.parm("prim_to_detail_pattern").eval().strip() + if not pattern: + cls.log.debug( + "Alembic ROP has no 'Primitive to Detail' pattern. " + "Validation is ignored.." + ) + return + + build_from_path = rop_node.parm("build_from_path").eval() + if not build_from_path: + cls.log.debug( + "Alembic ROP has 'Build from Path' disabled. " + "Validation is ignored.." + ) + return + + path_attr = rop_node.parm("path_attrib").eval() + if not path_attr: + cls.log.error( + "The Alembic ROP node has no Path Attribute" + "value set, but 'Build Hierarchy from Attribute'" + "is enabled." + ) + return [rop_node.path()] + + # Let's assume each attribute is explicitly named for now and has no + # wildcards for Primitive to Detail. This simplifies the check. + 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) + + # If there are no primitives on the start frame then it might be + # something that is emitted over time. As such we can't actually + # validate whether the attributes exist, because they won't exist + # yet. In that case, just warn the user and allow it. + if len(geo.iterPrims()) == 0: + cls.log.warning( + "No primitives found on current frame. Validation" + " for Primitive to Detail will be skipped." + ) + return + + attrib = geo.findPrimAttrib(path_attr) + if not attrib: + cls.log.info( + "Geometry Primitives are missing " + "path attribute: `%s`" % path_attr + ) + return [output_node.path()] + + # Ensure at least a single string value is present + if not attrib.strings(): + cls.log.info( + "Primitive path attribute has no " + "string values: %s" % path_attr + ) + return [output_node.path()] + + paths = None + for attr in pattern.split(" "): + if not attr.strip(): + # Ignore empty values + continue + + # Check if the primitive attribute exists + attrib = geo.findPrimAttrib(attr) + if not attrib: + # It is allowed to not have the attribute at all + continue + + # The issue can only happen if at least one string attribute is + # present. So we ignore cases with no values whatsoever. + if not attrib.strings(): + continue + + check = defaultdict(set) + values = geo.primStringAttribValues(attr) + if paths is None: + paths = geo.primStringAttribValues(path_attr) + + for path, value in zip(paths, values): + check[path].add(value) + + for path, values in check.items(): + # Whenever a single path has multiple values for the + # Primitive to Detail attribute then we consider it + # inconsistent and invalidate the ROP node's content. + if len(values) > 1: + cls.log.warning( + "Path has multiple values: %s (path: %s)" + % (list(values), path) + ) + return [output_node.path()] From 94ec68ab21182bbb01258a9e73ccb01b10a5a1e8 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 19 Jul 2023 00:13:51 +0300 Subject: [PATCH 012/327] update review validator --- .../publish/validate_review_colorspace.py | 153 ++++++------------ 1 file changed, 49 insertions(+), 104 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py index 18ca39234c..47c1e886d1 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py +++ b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py @@ -1,143 +1,88 @@ # -*- coding: utf-8 -*- import pyblish.api - -from collections import defaultdict from openpype.pipeline import PublishValidationError +from openpype.pipeline.publish import RepairAction +from openpype.hosts.houdini.api.action import SelectROPAction + + +class SetDefaultViewSpaceAction(RepairAction): + label = "Set default view space" + icon = "mdi.monitor" class ValidateReviewColorspace(pyblish.api.InstancePlugin): """Validate Review Colorspace parameters. - + It checks if 'OCIO Colorspace' parameter was set to valid value. """ order = pyblish.api.ValidatorOrder + 0.1 families = ["review"] hosts = ["houdini"] label = "Validate Review Colorspace" + actions = [SetDefaultViewSpaceAction, SelectROPAction] def process(self, instance): invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( - ("Colorspace parameter is not valid."), + ("'OCIO Colorspace' parameter is not valid."), title=self.label ) @classmethod def get_invalid(cls, instance): import hou # noqa - output_node = instance.data.get("output_node") + rop_node = hou.node(instance.data["instance_node"]) - if output_node is None: - cls.log.error( - "SOP Output node in '%s' does not exist. " - "Ensure a valid SOP output path is set." % rop_node.path() - ) - - return [rop_node.path()] - - pattern = rop_node.parm("prim_to_detail_pattern").eval().strip() - if not pattern: - cls.log.debug( - "Alembic ROP has no 'Primitive to Detail' pattern. " - "Validation is ignored.." - ) - return - - build_from_path = rop_node.parm("build_from_path").eval() - if not build_from_path: - cls.log.debug( - "Alembic ROP has 'Build from Path' disabled. " - "Validation is ignored.." - ) - return - - path_attr = rop_node.parm("path_attrib").eval() - if not path_attr: - cls.log.error( - "The Alembic ROP node has no Path Attribute" - "value set, but 'Build Hierarchy from Attribute'" - "is enabled." - ) - return [rop_node.path()] - - # Let's assume each attribute is explicitly named for now and has no - # wildcards for Primitive to Detail. This simplifies the check. - 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) - - # If there are no primitives on the start frame then it might be - # something that is emitted over time. As such we can't actually - # validate whether the attributes exist, because they won't exist - # yet. In that case, just warn the user and allow it. - if len(geo.iterPrims()) == 0: + if hou.Color.ocio_defaultDisplay() == "default": cls.log.warning( - "No primitives found on current frame. Validation" - " for Primitive to Detail will be skipped." + "Default Houdini colorspace is used, " + " skipping check.." ) return - attrib = geo.findPrimAttrib(path_attr) - if not attrib: - cls.log.info( - "Geometry Primitives are missing " - "path attribute: `%s`" % path_attr + if rop_node.evalParm("colorcorrect") != 2: + # any colorspace settings other than default requires + # 'Color Correct' parm to be set to 'OpenColorIO' + rop_node.setParms({"colorcorrect": 2}) + cls.log.debug( + "'Color Correct' parm on '%s' has been set to" + " 'OpenColorIO'", rop_node ) - return [output_node.path()] - # Ensure at least a single string value is present - if not attrib.strings(): - cls.log.info( - "Primitive path attribute has no " - "string values: %s" % path_attr + if rop_node.evalParm("ociocolorspace") not in \ + hou.Color.ocio_spaces(): + + cls.log.error( + "'OCIO Colorspace' value on '%s' is not valid, " + "select a valid option from the dropdown menu.", + rop_node ) - return [output_node.path()] + return rop_node - paths = None - for attr in pattern.split(" "): - if not attr.strip(): - # Ignore empty values - continue + @classmethod + def repair(cls, instance): + """Set Default View Space Action. - # Check if the primitive attribute exists - attrib = geo.findPrimAttrib(attr) - if not attrib: - # It is allowed to not have the attribute at all - continue + It is a helper action more than a repair action, + used to set colorspace on opengl node to the default view. + """ - # The issue can only happen if at least one string attribute is - # present. So we ignore cases with no values whatsoever. - if not attrib.strings(): - continue + import hou + import PyOpenColorIO as OCIO - check = defaultdict(set) - values = geo.primStringAttribValues(attr) - if paths is None: - paths = geo.primStringAttribValues(path_attr) + rop_node = hou.node(instance.data["instance_node"]) - for path, value in zip(paths, values): - check[path].add(value) + config = OCIO.GetCurrentConfig() + display = hou.Color.ocio_defaultDisplay() + view = hou.Color.ocio_defaultView() - for path, values in check.items(): - # Whenever a single path has multiple values for the - # Primitive to Detail attribute then we consider it - # inconsistent and invalidate the ROP node's content. - if len(values) > 1: - cls.log.warning( - "Path has multiple values: %s (path: %s)" - % (list(values), path) - ) - return [output_node.path()] + default_view_space = config.getDisplayColorSpaceName( + display, view) + + rop_node.setParms({"ociocolorspace" : default_view_space}) + cls.log.debug( + "'OCIO Colorspace' parm on '%s' has been set to '%s'", + default_view_space, rop_node + ) From 4d79320cf4f1abdd05dc3ce6fc46d6ccb30b2aca Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 19 Jul 2023 00:50:43 +0300 Subject: [PATCH 013/327] make hound happy --- .../hosts/houdini/plugins/publish/validate_review_colorspace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py index 47c1e886d1..02284dc641 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py +++ b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py @@ -81,7 +81,7 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin): default_view_space = config.getDisplayColorSpaceName( display, view) - rop_node.setParms({"ociocolorspace" : default_view_space}) + rop_node.setParms({"ociocolorspace": default_view_space}) cls.log.debug( "'OCIO Colorspace' parm on '%s' has been set to '%s'", default_view_space, rop_node From f4a5858edbed465f11908d18697b5dc0c6414b76 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 19 Jul 2023 19:43:39 +0300 Subject: [PATCH 014/327] update validator --- .../houdini/plugins/publish/validate_review_colorspace.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py index 02284dc641..8f3799cde4 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py +++ b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py @@ -33,9 +33,10 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance): import hou # noqa + import os rop_node = hou.node(instance.data["instance_node"]) - if hou.Color.ocio_defaultDisplay() == "default": + if os.getenv("OCIO") is None: cls.log.warning( "Default Houdini colorspace is used, " " skipping check.." @@ -78,8 +79,8 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin): display = hou.Color.ocio_defaultDisplay() view = hou.Color.ocio_defaultView() - default_view_space = config.getDisplayColorSpaceName( - display, view) + default_view_space = config.getDisplayViewColorSpaceName( + display, view) # works with PyOpenColorIO 2.2.1 rop_node.setParms({"ociocolorspace": default_view_space}) cls.log.debug( From 00d4afd1178d4ee95ee8d09a4a8c8348f3948606 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 19 Jul 2023 19:44:45 +0300 Subject: [PATCH 015/327] make hound happy --- .../hosts/houdini/plugins/publish/validate_review_colorspace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py index 8f3799cde4..addfa05bf1 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py +++ b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py @@ -80,7 +80,7 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin): view = hou.Color.ocio_defaultView() default_view_space = config.getDisplayViewColorSpaceName( - display, view) # works with PyOpenColorIO 2.2.1 + display, view) # works with PyOpenColorIO 2.2.1 rop_node.setParms({"ociocolorspace": default_view_space}) cls.log.debug( From d90d45c56df70e876d02acbf9d4e09aa1b6f5746 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 20 Jul 2023 13:38:09 +0300 Subject: [PATCH 016/327] move get function to colorspace.py --- .../publish/validate_review_colorspace.py | 19 +++++--- openpype/pipeline/colorspace.py | 36 ++++++++++++++++ openpype/scripts/ocio_wrapper.py | 43 +++++++++++++++++++ 3 files changed, 91 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py index addfa05bf1..cfc5a5d71d 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py +++ b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py @@ -71,19 +71,24 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin): """ import hou - import PyOpenColorIO as OCIO + from openpype.pipeline.colorspace import get_display_view_colorspace_name + from openpype.hosts.houdini.api.lib import get_color_management_preferences #noqa rop_node = hou.node(instance.data["instance_node"]) - config = OCIO.GetCurrentConfig() - display = hou.Color.ocio_defaultDisplay() - view = hou.Color.ocio_defaultView() + data = get_color_management_preferences() + config_path = data.get("config") + display = data.get("display") + view = data.get("view") - default_view_space = config.getDisplayViewColorSpaceName( - display, view) # works with PyOpenColorIO 2.2.1 + cls.log.debug("Get default view colorspace name..") + + default_view_space = get_display_view_colorspace_name(config_path, + display, view) rop_node.setParms({"ociocolorspace": default_view_space}) cls.log.debug( - "'OCIO Colorspace' parm on '%s' has been set to '%s'", + "'OCIO Colorspace' parm on '%s' has been set to " + "the default view color space '%s'", default_view_space, rop_node ) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 3f2d4891c1..a1d86b2fec 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -589,3 +589,39 @@ def _get_imageio_settings(project_settings, host_name): imageio_host = project_settings.get(host_name, {}).get("imageio", {}) return imageio_global, imageio_host + +def get_display_view_colorspace_name(config_path, display, view): + + if not compatibility_check(): + # python environment is not compatible with PyOpenColorIO + # needs to be run in subprocess + return get_display_view_colorspace_subprocess(config_path, + display, view) + + from openpype.scripts.ocio_wrapper import _get_display_view_colorspace_name #noqa + + return _get_display_view_colorspace_name(config_path, display, view) + +def get_display_view_colorspace_subprocess(config_path, display, view): + with _make_temp_json_file() as tmp_json_path: + # Prepare subprocess arguments + args = [ + "run", get_ocio_config_script_path(), + "config", "get_display_view_colorspace_name", + "--in_path", config_path, + "--out_path", tmp_json_path, + "--display", display, + "--view", view + + ] + log.info("Executing: {}".format(" ".join(args))) + + process_kwargs = { + "logger": log + } + + run_openpype_process(*args, **process_kwargs) + + # return all colorspaces + return_json_data = open(tmp_json_path).read() + return json.loads(return_json_data) diff --git a/openpype/scripts/ocio_wrapper.py b/openpype/scripts/ocio_wrapper.py index 16558642c6..ca703fd65c 100644 --- a/openpype/scripts/ocio_wrapper.py +++ b/openpype/scripts/ocio_wrapper.py @@ -173,6 +173,49 @@ def _get_views_data(config_path): return data +def _get_display_view_colorspace_name(config_path, display, view): + config_path = Path(config_path) + + if not config_path.is_file(): + raise IOError("Input path should be `config.ocio` file") + + config = ocio.Config().CreateFromFile(str(config_path)) + colorspace = config.getDisplayViewColorSpaceName(display, view) + + return colorspace + +@config.command( + name="get_display_view_colorspace_name", + help=( + "return default view colorspace name " + "for the given display and view " + "--path input arg is required" + ) +) +@click.option("--in_path", required=True, + help="path where to read ocio config file", + type=click.Path(exists=True)) +@click.option("--out_path", required=True, + help="path where to write output json file", + type=click.Path()) +@click.option("--display", required=True, + help="display", + type=click.STRING) +@click.option("--view", required=True, + help="view", + type=click.STRING) +def get_display_view_colorspace_name(in_path, out_path, + display, view): + + json_path = Path(out_path) + + out_data = _get_display_view_colorspace_name(in_path, + display, view) + + with open(json_path, "w") as f: + json.dump(out_data, f) + + print(f"Viewer data are saved to '{json_path}'") if __name__ == '__main__': main() From 2bb11d956ca2ec629f61d5d14e0b2ef4130a0363 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 20 Jul 2023 13:54:46 +0300 Subject: [PATCH 017/327] add doc strings --- .../publish/validate_review_colorspace.py | 2 +- openpype/pipeline/colorspace.py | 22 ++++++++++++ openpype/scripts/ocio_wrapper.py | 34 +++++++++++++++++-- 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py index cfc5a5d71d..67e29e0ee2 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py +++ b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py @@ -71,7 +71,7 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin): """ import hou - from openpype.pipeline.colorspace import get_display_view_colorspace_name + from openpype.pipeline.colorspace import get_display_view_colorspace_name #noqa from openpype.hosts.houdini.api.lib import get_color_management_preferences #noqa rop_node = hou.node(instance.data["instance_node"]) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index a1d86b2fec..22e8175a7e 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -591,6 +591,16 @@ def _get_imageio_settings(project_settings, host_name): return imageio_global, imageio_host def get_display_view_colorspace_name(config_path, display, view): + """get view colorspace name for the given display and view. + + Args: + config_path (str): path string leading to config.ocio + display (str): display name e.g. "ACES" + view (str): view name e.g. "sRGB" + + Returns: + view color space name (str) e.g. "Output - sRGB" + """ if not compatibility_check(): # python environment is not compatible with PyOpenColorIO @@ -603,6 +613,18 @@ def get_display_view_colorspace_name(config_path, display, view): return _get_display_view_colorspace_name(config_path, display, view) def get_display_view_colorspace_subprocess(config_path, display, view): + """get view colorspace name for the given display and view + via subprocess. + + Args: + config_path (str): path string leading to config.ocio + display (str): display name e.g. "ACES" + view (str): view name e.g. "sRGB" + + Returns: + view color space name (str) e.g. "Output - sRGB" + """ + with _make_temp_json_file() as tmp_json_path: # Prepare subprocess arguments args = [ diff --git a/openpype/scripts/ocio_wrapper.py b/openpype/scripts/ocio_wrapper.py index ca703fd65c..f94faabe11 100644 --- a/openpype/scripts/ocio_wrapper.py +++ b/openpype/scripts/ocio_wrapper.py @@ -174,6 +174,21 @@ def _get_views_data(config_path): return data def _get_display_view_colorspace_name(config_path, display, view): + """get view colorspace name for the given display and view. + + Args: + config_path (str): path string leading to config.ocio + display (str): display name e.g. "ACES" + view (str): view name e.g. "sRGB" + + + Raises: + IOError: Input config does not exist. + + Returns: + view color space name (str) e.g. "Output - sRGB" + """ + config_path = Path(config_path) if not config_path.is_file(): @@ -199,13 +214,28 @@ def _get_display_view_colorspace_name(config_path, display, view): help="path where to write output json file", type=click.Path()) @click.option("--display", required=True, - help="display", + help="display name", type=click.STRING) @click.option("--view", required=True, - help="view", + help="view name", type=click.STRING) def get_display_view_colorspace_name(in_path, out_path, display, view): + """Aggregate view colorspace name to file. + + Python 2 wrapped console command + + Args: + in_path (str): config file path string + out_path (str): temp json file path string + display (str): display name e.g. "ACES" + view (str): view name e.g. "sRGB" + + Example of use: + > pyton.exe ./ocio_wrapper.py config \ + get_display_view_colorspace_name --in_path= \ + --out_path= --display= --view= + """ json_path = Path(out_path) From 1a6e7aa041cfdaffbae58ddfada7e03c753b89b5 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 1 Aug 2023 22:05:16 +0800 Subject: [PATCH 018/327] adding OpenpypeData parameter as custom attributes back to the loaded objects --- openpype/hosts/max/api/pipeline.py | 25 ++++++++++++++++++ .../hosts/max/plugins/load/load_camera_fbx.py | 26 ++++++++----------- .../hosts/max/plugins/load/load_max_scene.py | 16 +++++++----- openpype/hosts/max/plugins/load/load_model.py | 18 +++++++++---- .../hosts/max/plugins/load/load_model_fbx.py | 9 ++++--- .../hosts/max/plugins/load/load_model_obj.py | 12 +++++---- .../hosts/max/plugins/load/load_model_usd.py | 11 +++++--- .../hosts/max/plugins/load/load_pointcache.py | 15 ++++++++--- .../hosts/max/plugins/load/load_pointcloud.py | 15 ++++++----- .../max/plugins/load/load_redshift_proxy.py | 5 ++-- 10 files changed, 101 insertions(+), 51 deletions(-) diff --git a/openpype/hosts/max/api/pipeline.py b/openpype/hosts/max/api/pipeline.py index 03b85a4066..82470dd510 100644 --- a/openpype/hosts/max/api/pipeline.py +++ b/openpype/hosts/max/api/pipeline.py @@ -15,8 +15,10 @@ from openpype.pipeline import ( ) from openpype.hosts.max.api.menu import OpenPypeMenu from openpype.hosts.max.api import lib +from openpype.hosts.max.api.plugin import MS_CUSTOM_ATTRIB from openpype.hosts.max import MAX_HOST_DIR + from pymxs import runtime as rt # noqa log = logging.getLogger("openpype.hosts.max") @@ -170,3 +172,26 @@ def containerise(name: str, nodes: list, context, loader=None, suffix="_CON"): if not lib.imprint(container_name, data): print(f"imprinting of {container_name} failed.") return container + + +def load_OpenpypeData(container, loaded_nodes): + """Function to load the OpenpypeData Parameter along with + the published objects + + Args: + container (str): target container to set up + the custom attributes + loaded_nodes (list): list of nodes to be loaded + """ + attrs = rt.Execute(MS_CUSTOM_ATTRIB) + if rt.custAttributes.get(container.baseObject, attrs): + rt.custAttributes.delete(container.baseObject, attrs) + rt.custAttributes.add(container.baseObject, attrs) + node_list = [] + for i in loaded_nodes: + node_ref = rt.NodeTransformMonitor(node=i) + node_list.append(node_ref) + + # Setting the property + rt.setProperty( + container.openPypeData, "all_handles", node_list) diff --git a/openpype/hosts/max/plugins/load/load_camera_fbx.py b/openpype/hosts/max/plugins/load/load_camera_fbx.py index 62284b23d9..6b16bfe474 100644 --- a/openpype/hosts/max/plugins/load/load_camera_fbx.py +++ b/openpype/hosts/max/plugins/load/load_camera_fbx.py @@ -1,7 +1,7 @@ import os from openpype.hosts.max.api import lib, maintained_selection -from openpype.hosts.max.api.pipeline import containerise +from openpype.hosts.max.api.pipeline import containerise, load_OpenpypeData from openpype.pipeline import get_representation_path, load @@ -32,8 +32,9 @@ class FbxLoader(load.LoaderPlugin): if not container: container = rt.Container() container.name = f"{name}" - - for selection in rt.GetCurrentSelection(): + selections = rt.GetCurrentSelection() + load_OpenpypeData(container, selections) + for selection in selections: selection.Parent = container return containerise( @@ -45,18 +46,13 @@ class FbxLoader(load.LoaderPlugin): path = get_representation_path(representation) node = rt.GetNodeByName(container["instance_node"]) rt.Select(node.Children) - fbx_reimport_cmd = ( - f""" - -FBXImporterSetParam "Animation" true -FBXImporterSetParam "Cameras" true -FBXImporterSetParam "AxisConversionMethod" true -FbxExporterSetParam "UpAxis" "Y" -FbxExporterSetParam "Preserveinstances" true - -importFile @"{path}" #noPrompt using:FBXIMP - """) - rt.Execute(fbx_reimport_cmd) + rt.FBXImporterSetParam("Animation", True) + rt.FBXImporterSetParam("Camera", True) + rt.FBXImporterSetParam("AxisConversionMethod", True) + rt.FBXImporterSetParam("Preserveinstances", True) + rt.ImportFile( + path, rt.name("noPrompt"), using=rt.FBXIMP) + load_OpenpypeData(node, node.Children) with maintained_selection(): rt.Select(node) diff --git a/openpype/hosts/max/plugins/load/load_max_scene.py b/openpype/hosts/max/plugins/load/load_max_scene.py index 76cd3bf367..468461bc0e 100644 --- a/openpype/hosts/max/plugins/load/load_max_scene.py +++ b/openpype/hosts/max/plugins/load/load_max_scene.py @@ -1,7 +1,8 @@ import os from openpype.hosts.max.api import lib -from openpype.hosts.max.api.pipeline import containerise +from openpype.hosts.max.api.pipeline import containerise, load_OpenpypeData + from openpype.pipeline import get_representation_path, load @@ -27,6 +28,7 @@ class MaxSceneLoader(load.LoaderPlugin): rt.MergeMaxFile(path) max_objects = rt.getLastMergedNodes() max_container = rt.Container(name=f"{name}") + load_OpenpypeData(max_container, max_objects) for max_object in max_objects: max_object.Parent = max_container @@ -39,16 +41,16 @@ class MaxSceneLoader(load.LoaderPlugin): path = get_representation_path(representation) node_name = container["instance_node"] - rt.MergeMaxFile(path, - rt.Name("noRedraw"), - rt.Name("deleteOldDups"), - rt.Name("useSceneMtlDups")) + rt.MergeMaxFile(path) max_objects = rt.getLastMergedNodes() container_node = rt.GetNodeByName(node_name) + instance_name, _ = os.path.splitext(node_name) + instance_container = rt.GetNodeByName(instance_name) for max_object in max_objects: - max_object.Parent = container_node - + max_object.Parent = instance_container + instance_container.Parent = container_node + load_OpenpypeData(container_node, max_objects) lib.imprint(container["instance_node"], { "representation": str(representation["_id"]) }) diff --git a/openpype/hosts/max/plugins/load/load_model.py b/openpype/hosts/max/plugins/load/load_model.py index cff82a593c..810fc65968 100644 --- a/openpype/hosts/max/plugins/load/load_model.py +++ b/openpype/hosts/max/plugins/load/load_model.py @@ -1,6 +1,6 @@ import os from openpype.pipeline import load, get_representation_path -from openpype.hosts.max.api.pipeline import containerise +from openpype.hosts.max.api.pipeline import containerise, load_OpenpypeData from openpype.hosts.max.api import lib from openpype.hosts.max.api.lib import maintained_selection @@ -45,7 +45,10 @@ class ModelAbcLoader(load.LoaderPlugin): self.log.error("Something failed when loading.") abc_container = abc_containers.pop() - + selections = rt.GetCurrentSelection() + abc_selections = [abc for abc in selections + if abc.name != "Alembic"] + load_OpenpypeData(abc_container, abc_selections) return containerise( name, [abc_container], context, loader=self.__class__.__name__ ) @@ -57,6 +60,10 @@ class ModelAbcLoader(load.LoaderPlugin): node = rt.GetNodeByName(container["instance_node"]) rt.Select(node.Children) + nodes_list = [] + with maintained_selection(): + rt.Select(node) + for alembic in rt.Selection: abc = rt.GetNodeByName(alembic.name) rt.Select(abc.Children) @@ -67,9 +74,10 @@ class ModelAbcLoader(load.LoaderPlugin): for abc_obj in rt.Selection: alembic_obj = rt.GetNodeByName(abc_obj.name) alembic_obj.source = path - - with maintained_selection(): - rt.Select(node) + nodes_list.append(alembic_obj) + abc_selections = [abc for abc in nodes_list + if abc.name != "Alembic"] + load_OpenpypeData(node, abc_selections) lib.imprint( container["instance_node"], diff --git a/openpype/hosts/max/plugins/load/load_model_fbx.py b/openpype/hosts/max/plugins/load/load_model_fbx.py index 12f526ab95..8f2b4f4ac3 100644 --- a/openpype/hosts/max/plugins/load/load_model_fbx.py +++ b/openpype/hosts/max/plugins/load/load_model_fbx.py @@ -1,6 +1,6 @@ import os from openpype.pipeline import load, get_representation_path -from openpype.hosts.max.api.pipeline import containerise +from openpype.hosts.max.api.pipeline import containerise, load_OpenpypeData from openpype.hosts.max.api import lib from openpype.hosts.max.api.lib import maintained_selection @@ -28,7 +28,10 @@ class FbxModelLoader(load.LoaderPlugin): container = rt.Container() container.name = name - for selection in rt.GetCurrentSelection(): + selections = rt.GetCurrentSelection() + load_OpenpypeData(container, selections) + + for selection in selections: selection.Parent = container return containerise( @@ -47,7 +50,7 @@ class FbxModelLoader(load.LoaderPlugin): rt.FBXImporterSetParam("UpAxis", "Y") rt.FBXImporterSetParam("Preserveinstances", True) rt.importFile(path, rt.name("noPrompt"), using=rt.FBXIMP) - + load_OpenpypeData(container, node.Children) with maintained_selection(): rt.Select(node) diff --git a/openpype/hosts/max/plugins/load/load_model_obj.py b/openpype/hosts/max/plugins/load/load_model_obj.py index 18a19414fa..83b5ec49b9 100644 --- a/openpype/hosts/max/plugins/load/load_model_obj.py +++ b/openpype/hosts/max/plugins/load/load_model_obj.py @@ -2,7 +2,7 @@ import os from openpype.hosts.max.api import lib from openpype.hosts.max.api.lib import maintained_selection -from openpype.hosts.max.api.pipeline import containerise +from openpype.hosts.max.api.pipeline import containerise, load_OpenpypeData from openpype.pipeline import get_representation_path, load @@ -25,9 +25,10 @@ class ObjLoader(load.LoaderPlugin): # create "missing" container for obj import container = rt.Container() container.name = name - + selections = rt.GetCurrentSelection() + load_OpenpypeData(container, selections) # get current selection - for selection in rt.GetCurrentSelection(): + for selection in selections: selection.Parent = container asset = rt.GetNodeByName(name) @@ -49,9 +50,10 @@ class ObjLoader(load.LoaderPlugin): rt.Execute(f'importFile @"{path}" #noPrompt using:ObjImp') # get current selection - for selection in rt.GetCurrentSelection(): + selections = rt.GetCurrentSelection() + for selection in selections: selection.Parent = container - + load_OpenpypeData(container, container.Children) with maintained_selection(): rt.Select(node) diff --git a/openpype/hosts/max/plugins/load/load_model_usd.py b/openpype/hosts/max/plugins/load/load_model_usd.py index 48b50b9b18..a1961e6d89 100644 --- a/openpype/hosts/max/plugins/load/load_model_usd.py +++ b/openpype/hosts/max/plugins/load/load_model_usd.py @@ -2,7 +2,7 @@ import os from openpype.hosts.max.api import lib from openpype.hosts.max.api.lib import maintained_selection -from openpype.hosts.max.api.pipeline import containerise +from openpype.hosts.max.api.pipeline import containerise, load_OpenpypeData from openpype.pipeline import get_representation_path, load @@ -30,8 +30,10 @@ class ModelUSDLoader(load.LoaderPlugin): rt.LogLevel = rt.Name("info") rt.USDImporter.importFile(filepath, importOptions=import_options) - + selections = rt.GetCurrentSelection() asset = rt.GetNodeByName(name) + mesh_selections = [r for r in selections if r != asset] + load_OpenpypeData(asset, mesh_selections) return containerise( name, [asset], context, loader=self.__class__.__name__) @@ -55,11 +57,12 @@ class ModelUSDLoader(load.LoaderPlugin): rt.LogPath = log_filepath rt.LogLevel = rt.Name("info") - rt.USDImporter.importFile(path, - importOptions=import_options) + rt.USDImporter.importFile( + path, importOptions=import_options) asset = rt.GetNodeByName(instance_name) asset.Parent = node + load_OpenpypeData(asset, asset.Children) with maintained_selection(): rt.Select(node) diff --git a/openpype/hosts/max/plugins/load/load_pointcache.py b/openpype/hosts/max/plugins/load/load_pointcache.py index 290503e053..026938feff 100644 --- a/openpype/hosts/max/plugins/load/load_pointcache.py +++ b/openpype/hosts/max/plugins/load/load_pointcache.py @@ -7,7 +7,7 @@ Because of limited api, alembics can be only loaded, but not easily updated. import os from openpype.pipeline import load, get_representation_path from openpype.hosts.max.api import lib, maintained_selection -from openpype.hosts.max.api.pipeline import containerise +from openpype.hosts.max.api.pipeline import containerise, load_OpenpypeData class AbcLoader(load.LoaderPlugin): @@ -48,11 +48,15 @@ class AbcLoader(load.LoaderPlugin): self.log.error("Something failed when loading.") abc_container = abc_containers.pop() - - for abc in rt.GetCurrentSelection(): + selections = rt.GetCurrentSelection() + abc_selections = [abc for abc in selections + if abc.name != "Alembic"] + load_OpenpypeData(abc_container, abc_selections) + for abc in selections: for cam_shape in abc.Children: cam_shape.playbackType = 2 + return containerise( name, [abc_container], context, loader=self.__class__.__name__ ) @@ -71,7 +75,7 @@ class AbcLoader(load.LoaderPlugin): container["instance_node"], {"representation": str(representation["_id"])}, ) - + nodes_list = [] with maintained_selection(): rt.Select(node.Children) @@ -85,6 +89,9 @@ class AbcLoader(load.LoaderPlugin): for abc_obj in rt.Selection: alembic_obj = rt.GetNodeByName(abc_obj.name) alembic_obj.source = path + nodes_list.append(alembic_obj) + abc_selections = [abc for abc in nodes_list if abc.name != "Alembic"] + load_OpenpypeData(node, abc_selections) def switch(self, container, representation): self.update(container, representation) diff --git a/openpype/hosts/max/plugins/load/load_pointcloud.py b/openpype/hosts/max/plugins/load/load_pointcloud.py index 2a1175167a..18998f4529 100644 --- a/openpype/hosts/max/plugins/load/load_pointcloud.py +++ b/openpype/hosts/max/plugins/load/load_pointcloud.py @@ -1,7 +1,7 @@ import os from openpype.hosts.max.api import lib, maintained_selection -from openpype.hosts.max.api.pipeline import containerise +from openpype.hosts.max.api.pipeline import containerise, load_OpenpypeData from openpype.pipeline import get_representation_path, load @@ -21,8 +21,11 @@ class PointCloudLoader(load.LoaderPlugin): filepath = os.path.normpath(self.filepath_from_context(context)) obj = rt.tyCache() obj.filename = filepath - prt_container = rt.GetNodeByName(obj.name) + prt_container = rt.container() + prt_container.name = name + obj.Parent = prt_container + load_OpenpypeData(prt_container, [obj]) return containerise( name, [prt_container], context, loader=self.__class__.__name__) @@ -38,10 +41,10 @@ class PointCloudLoader(load.LoaderPlugin): for prt in rt.Selection: prt_object = rt.GetNodeByName(prt.name) prt_object.filename = path - - lib.imprint(container["instance_node"], { - "representation": str(representation["_id"]) - }) + load_OpenpypeData(node, node.Children) + lib.imprint(container["instance_node"], { + "representation": str(representation["_id"]) + }) def switch(self, container, representation): self.update(container, representation) diff --git a/openpype/hosts/max/plugins/load/load_redshift_proxy.py b/openpype/hosts/max/plugins/load/load_redshift_proxy.py index 31692f6367..b62400d2e5 100644 --- a/openpype/hosts/max/plugins/load/load_redshift_proxy.py +++ b/openpype/hosts/max/plugins/load/load_redshift_proxy.py @@ -5,7 +5,7 @@ from openpype.pipeline import ( load, get_representation_path ) -from openpype.hosts.max.api.pipeline import containerise +from openpype.hosts.max.api.pipeline import containerise, load_OpenpypeData from openpype.hosts.max.api import lib @@ -33,7 +33,7 @@ class RedshiftProxyLoader(load.LoaderPlugin): container = rt.container() container.name = name rs_proxy.Parent = container - + load_OpenpypeData(container, [rs_proxy]) asset = rt.getNodeByName(name) return containerise( @@ -49,6 +49,7 @@ class RedshiftProxyLoader(load.LoaderPlugin): for proxy in children_node.Children: proxy.file = path + load_OpenpypeData(node, node.Children) lib.imprint(container["instance_node"], { "representation": str(representation["_id"]) }) From 1b8822a4e0c6a094d77eaa322b98d3b7338921c3 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 1 Aug 2023 17:02:23 +0200 Subject: [PATCH 019/327] nuke: removing `customOCIOConfigPath` value in workfile Linux is reversing processing order of preference the way: if workfile is having set value in `customOCIOConfigPath` use it even OCIO env variable is set to some value. This way we are making sure OCIO is read only. --- openpype/hosts/nuke/api/lib.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 364c8eeff4..54e46996d6 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2070,9 +2070,15 @@ class WorkfileSettings(object): str(workfile_settings["OCIO_config"])) else: - # set values to root + # OCIO config path is defined from prelaunch hook self._root_node["colorManagement"].setValue("OCIO") + # restart settings in case some were set previously + # linux is reversing order of preference to prefer what ever + # is set knobs before it apply it form environment variable + if self._root_node["customOCIOConfigPath"].value(): + self._root_node["customOCIOConfigPath"].setValue("") + # we dont need the key anymore workfile_settings.pop("customOCIOConfigPath", None) workfile_settings.pop("colorManagement", None) From 4f9dd21cffe64a29fa49a9a7d76cd2dd5b42f0a0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 2 Aug 2023 10:49:39 +0200 Subject: [PATCH 020/327] removing OCIO env var, since it is not working this way --- .../deadline/plugins/publish/submit_houdini_render_deadline.py | 1 - openpype/modules/deadline/plugins/publish/submit_max_deadline.py | 1 - .../modules/deadline/plugins/publish/submit_maya_deadline.py | 1 - .../modules/deadline/plugins/publish/submit_nuke_deadline.py | 1 - openpype/modules/deadline/plugins/publish/submit_publish_job.py | 1 - 5 files changed, 5 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py index 1f4770653c..af341ca8e8 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -88,7 +88,6 @@ class HoudiniSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): "AVALON_APP_NAME", "OPENPYPE_DEV", "OPENPYPE_LOG_NO_COLORS", - "OCIO", ] # Add OpenPype version if we are running from build. diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index 2eb8518618..76fca078e9 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -112,7 +112,6 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, "AVALON_APP_NAME", "OPENPYPE_DEV", "IS_TEST", - "OCIO", ] # Add OpenPype version if we are running from build. diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py index 700d03519c..a0c324ff22 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -207,7 +207,6 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, "AVALON_APP_NAME", "OPENPYPE_DEV" "IS_TEST", - "OCIO", ] # Add OpenPype version if we are running from build. diff --git a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py index e52ee632c5..4900231783 100644 --- a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py @@ -316,7 +316,6 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin, "TOOL_ENV", "FOUNDRY_LICENSE", "OPENPYPE_SG_USER", - "OCIO", ] # Add OpenPype version if we are running from build. diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 963831289a..73a6866d5c 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -124,7 +124,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, "AVALON_APP_NAME", "OPENPYPE_USERNAME", "OPENPYPE_SG_USER", - "OCIO", ] # Add OpenPype version if we are running from build. From 069fd70546421fa0896fb50b957ec7afac9c795e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 2 Aug 2023 13:49:30 +0200 Subject: [PATCH 021/327] nuke: comunicate there was old residual path set in workfile --- openpype/hosts/nuke/api/lib.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 54e46996d6..cca370ac5e 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2073,11 +2073,12 @@ class WorkfileSettings(object): # OCIO config path is defined from prelaunch hook self._root_node["colorManagement"].setValue("OCIO") - # restart settings in case some were set previously - # linux is reversing order of preference to prefer what ever - # is set knobs before it apply it form environment variable - if self._root_node["customOCIOConfigPath"].value(): - self._root_node["customOCIOConfigPath"].setValue("") + # print previous settings in case some were found in workfile + residual_path = self._root_node["customOCIOConfigPath"].value() + if residual_path: + log.info("Residual OCIO config path found: `{}`".format( + residual_path + )) # we dont need the key anymore workfile_settings.pop("customOCIOConfigPath", None) From 979446ac537feaf15841d66a42841676e7b475d3 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 2 Aug 2023 14:08:45 +0200 Subject: [PATCH 022/327] nuke: adding ocio path to workfile making sure it is in environment variable --- openpype/hosts/nuke/api/lib.py | 121 +++++++++++++++++++++++++++++---- 1 file changed, 108 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index cca370ac5e..61e42d0d17 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2101,9 +2101,29 @@ class WorkfileSettings(object): # set ocio config path if config_data: - current_ocio_path = os.getenv("OCIO") - if current_ocio_path != config_data["path"]: - message = """ + log.info("OCIO config path found: `{}`".format( + config_data["path"])) + + # check if there's a mismatch between environment and settings + wrong_environment = self._is_settings_different_from_environment( + config_data) + + # if there's no mismatch between environment and settings + if not wrong_environment: + self._set_ocio_config_path_to_workfile(config_data) + + def _is_settings_different_from_environment(self, config_data): + """ Check if OCIO config path is different from environment + + Args: + config_data (dict): OCIO config data from settings + + Returns: + bool: True if there's a mismatch between environment and settings + """ + current_ocio_path = os.getenv("OCIO") + if current_ocio_path != config_data["path"]: + message = """ It seems like there's a mismatch between the OCIO config path set in your Nuke settings and the actual path set in your OCIO environment. @@ -2121,12 +2141,87 @@ Please note the paths for your reference: Reopening Nuke should synchronize these paths and resolve any discrepancies. """ - nuke.message( - message.format( - env_path=current_ocio_path, - settings_path=config_data["path"] - ) + nuke.message( + message.format( + env_path=current_ocio_path, + settings_path=config_data["path"] ) + ) + return True + + def _set_ocio_config_path_to_workfile(self, config_data): + """ Set OCIO config path to workfile + + Path set into nuke workfile. It is trying to replace path with + environment variable if possible. If not, it will set it as it is. + It also saves the script to apply the change, but only if it's not + empty Untitled script. + + Args: + config_data (dict): OCIO config data from settings + + """ + # replace path with env var if possible + ocio_path = self._replace_ocio_path_with_env_var( + config_data["path"] + ) + log.info("Setting OCIO config path to: `{}`".format( + ocio_path)) + + self._root_node["customOCIOConfigPath"].setValue( + ocio_path + ) + self._root_node["OCIO_config"].setValue("custom") + + # only save script if it's not empty + if self._root_node["name"].value() != "": + log.info("Saving script to apply OCIO config path change.") + nuke.scriptSave() + + def _replace_ocio_path_with_env_var(self, path): + """ Replace OCIO config path with environment variable + + Environment variable is added as TCL expression to path. TCL expression + is also replacing backward slashes found in path for windows + formatted values. + + Args: + path (str): OCIO config path + + Returns: + str: OCIO config path with environment variable + """ + # QUESTION: should we also include other names variants + included_vars = [ + "BUILTIN_OCIO_ROOT", + "OPENPYPE_PROJECT_ROOT" + ] + for env_var, env_path in os.environ.items(): + # first check if variable is whitelisted + if all(var_ not in env_var for var_ in included_vars): + # included vars not found in env_var name + continue + + # it has to be directory current process can see + if not os.path.isdir(env_path): + continue + + # make sure paths are in same format + env_path = env_path.replace("\\", "/") + path = path.replace("\\", "/") + + # check if env_path is in path and replace to first found positive + if env_path in path: + # with regsub we make sure path format of slashes is correct + resub_expr = ( + "[regsub -all {{\\\\}} [getenv {}] \"/\"]").format(env_var) + + new_path = path.replace( + env_path, resub_expr + ) + break + + return new_path def set_writes_colorspace(self): ''' Adds correct colorspace to write node dict @@ -2247,11 +2342,11 @@ Reopening Nuke should synchronize these paths and resolve any discrepancies. nuke_colorspace = get_nuke_imageio_settings() log.info("Setting colorspace to workfile...") - try: - self.set_root_colorspace(nuke_colorspace) - except AttributeError: - msg = "set_colorspace(): missing `workfile` settings in template" - nuke.message(msg) + # try: + self.set_root_colorspace(nuke_colorspace) + # except AttributeError: + # msg = "set_colorspace(): missing `workfile` settings in template" + # nuke.message(msg) log.info("Setting colorspace to viewers...") try: From 774a1c403137ea68df35d2eccace89d674a044bc Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 2 Aug 2023 14:58:46 +0200 Subject: [PATCH 023/327] nuke reverting changes --- .../modules/deadline/plugins/publish/submit_max_deadline.py | 2 +- .../modules/deadline/plugins/publish/submit_maya_deadline.py | 2 +- openpype/modules/deadline/plugins/publish/submit_publish_job.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index 76fca078e9..fff7a4ced5 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -111,7 +111,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, "AVALON_TASK", "AVALON_APP_NAME", "OPENPYPE_DEV", - "IS_TEST", + "IS_TEST" ] # Add OpenPype version if we are running from build. diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py index a0c324ff22..1dfb6e0e5c 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -206,7 +206,7 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, "AVALON_TASK", "AVALON_APP_NAME", "OPENPYPE_DEV" - "IS_TEST", + "IS_TEST" ] # Add OpenPype version if we are running from build. diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 73a6866d5c..2ed21c0621 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -123,7 +123,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, "FTRACK_SERVER", "AVALON_APP_NAME", "OPENPYPE_USERNAME", - "OPENPYPE_SG_USER", + "OPENPYPE_SG_USER" ] # Add OpenPype version if we are running from build. From 8bdf67b575daf68d0a15a5754a5328c122b12bf7 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 2 Aug 2023 17:16:28 +0100 Subject: [PATCH 024/327] Fix loading hero version for some assets --- .../unreal/plugins/load/load_alembic_animation.py | 8 ++++++-- .../unreal/plugins/load/load_skeletalmesh_abc.py | 8 ++++++-- .../unreal/plugins/load/load_skeletalmesh_fbx.py | 8 ++++++-- .../hosts/unreal/plugins/load/load_staticmesh_abc.py | 11 ++++++----- 4 files changed, 24 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_alembic_animation.py b/openpype/hosts/unreal/plugins/load/load_alembic_animation.py index cb60197a4c..a2aab59cec 100644 --- a/openpype/hosts/unreal/plugins/load/load_alembic_animation.py +++ b/openpype/hosts/unreal/plugins/load/load_alembic_animation.py @@ -76,11 +76,15 @@ class AnimationAlembicLoader(plugin.Loader): asset_name = "{}_{}".format(asset, name) else: asset_name = "{}".format(name) - version = context.get('version').get('name') + version = context.get('version') + if not version.get("name") and version.get('type') == "hero_version": + name_version = f"{name}_hero" + else: + name_version = f"{name}_v{version:03d}" tools = unreal.AssetToolsHelpers().get_asset_tools() asset_dir, container_name = tools.create_unique_asset_name( - f"{root}/{asset}/{name}_v{version:03d}", suffix="") + f"{root}/{asset}/{name_version}", suffix="") container_name += suffix diff --git a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py index 0b0030ff77..fc22d4f857 100644 --- a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py +++ b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py @@ -78,11 +78,15 @@ class SkeletalMeshAlembicLoader(plugin.Loader): asset_name = "{}_{}".format(asset, name) else: asset_name = "{}".format(name) - version = context.get('version').get('name') + version = context.get('version') + if not version.get("name") and version.get('type') == "hero_version": + name_version = f"{name}_hero" + else: + name_version = f"{name}_v{version:03d}" tools = unreal.AssetToolsHelpers().get_asset_tools() asset_dir, container_name = tools.create_unique_asset_name( - f"{root}/{asset}/{name}_v{version:03d}", suffix="") + f"{root}/{asset}/{name_version}", suffix="") container_name += suffix diff --git a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py index 09cd37b9db..0cf0bd58dc 100644 --- a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py +++ b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py @@ -52,11 +52,15 @@ class SkeletalMeshFBXLoader(plugin.Loader): asset_name = "{}_{}".format(asset, name) else: asset_name = "{}".format(name) - version = context.get('version').get('name') + version = context.get('version') + if not version.get("name") and version.get('type') == "hero_version": + name_version = f"{name}_hero" + else: + name_version = f"{name}_v{version:03d}" tools = unreal.AssetToolsHelpers().get_asset_tools() asset_dir, container_name = tools.create_unique_asset_name( - f"{root}/{asset}/{name}_v{version:03d}", suffix="") + f"{root}/{asset}/{name_version}", suffix="") container_name += suffix diff --git a/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py b/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py index 98e6d962b1..ffded49cd8 100644 --- a/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py +++ b/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py @@ -79,11 +79,12 @@ class StaticMeshAlembicLoader(plugin.Loader): root = "/Game/Ayon/Assets" asset = context.get('asset').get('name') suffix = "_CON" - if asset: - asset_name = "{}_{}".format(asset, name) + asset_name = f"{asset}_{name}" if asset else f"{name}" + version = context.get('version') + if not version.get("name") and version.get('type') == "hero_version": + name_version = f"{name}_hero" else: - asset_name = "{}".format(name) - version = context.get('version').get('name') + name_version = f"{name}_v{version:03d}" default_conversion = False if options.get("default_conversion"): @@ -91,7 +92,7 @@ class StaticMeshAlembicLoader(plugin.Loader): tools = unreal.AssetToolsHelpers().get_asset_tools() asset_dir, container_name = tools.create_unique_asset_name( - f"{root}/{asset}/{name}_v{version:03d}", suffix="") + f"{root}/{asset}/{name_version}", suffix="") container_name += suffix From 01cf1a45874fd8f8d19fd0b42ddfecaf82ad3f8a Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 2 Aug 2023 17:22:21 +0100 Subject: [PATCH 025/327] Add version number for static meshes from fbx --- openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py b/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py index fa26e252f5..c835bce136 100644 --- a/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py +++ b/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py @@ -78,10 +78,15 @@ class StaticMeshFBXLoader(plugin.Loader): asset_name = "{}_{}".format(asset, name) else: asset_name = "{}".format(name) + version = context.get('version') + if not version.get("name") and version.get('type') == "hero_version": + name_version = f"{name}_hero" + else: + name_version = f"{name}_v{version:03d}" tools = unreal.AssetToolsHelpers().get_asset_tools() asset_dir, container_name = tools.create_unique_asset_name( - f"{root}/{asset}/{name}", suffix="" + f"{root}/{asset}/{name_version}", suffix="" ) container_name += suffix From 9a0d1d73e0532fd07a82bb202ae1cb3964bb45b2 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 3 Aug 2023 09:44:37 +0100 Subject: [PATCH 026/327] Fix loading versioned assets --- openpype/hosts/unreal/plugins/load/load_alembic_animation.py | 2 +- openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py | 2 +- openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py | 2 +- openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py | 2 +- openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_alembic_animation.py b/openpype/hosts/unreal/plugins/load/load_alembic_animation.py index a2aab59cec..059c1515c0 100644 --- a/openpype/hosts/unreal/plugins/load/load_alembic_animation.py +++ b/openpype/hosts/unreal/plugins/load/load_alembic_animation.py @@ -80,7 +80,7 @@ class AnimationAlembicLoader(plugin.Loader): if not version.get("name") and version.get('type') == "hero_version": name_version = f"{name}_hero" else: - name_version = f"{name}_v{version:03d}" + name_version = f"{name}_v{version.get('name'):03d}" tools = unreal.AssetToolsHelpers().get_asset_tools() asset_dir, container_name = tools.create_unique_asset_name( diff --git a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py index fc22d4f857..8848722bd7 100644 --- a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py +++ b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py @@ -82,7 +82,7 @@ class SkeletalMeshAlembicLoader(plugin.Loader): if not version.get("name") and version.get('type') == "hero_version": name_version = f"{name}_hero" else: - name_version = f"{name}_v{version:03d}" + name_version = f"{name}_v{version.get('name'):03d}" tools = unreal.AssetToolsHelpers().get_asset_tools() asset_dir, container_name = tools.create_unique_asset_name( diff --git a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py index 0cf0bd58dc..6fb3476d89 100644 --- a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py +++ b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py @@ -56,7 +56,7 @@ class SkeletalMeshFBXLoader(plugin.Loader): if not version.get("name") and version.get('type') == "hero_version": name_version = f"{name}_hero" else: - name_version = f"{name}_v{version:03d}" + name_version = f"{name}_v{version.get('name'):03d}" tools = unreal.AssetToolsHelpers().get_asset_tools() asset_dir, container_name = tools.create_unique_asset_name( diff --git a/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py b/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py index ffded49cd8..20d9a31e03 100644 --- a/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py +++ b/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py @@ -84,7 +84,7 @@ class StaticMeshAlembicLoader(plugin.Loader): if not version.get("name") and version.get('type') == "hero_version": name_version = f"{name}_hero" else: - name_version = f"{name}_v{version:03d}" + name_version = f"{name}_v{version.get('name'):03d}" default_conversion = False if options.get("default_conversion"): diff --git a/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py b/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py index c835bce136..981003ece2 100644 --- a/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py +++ b/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py @@ -82,7 +82,7 @@ class StaticMeshFBXLoader(plugin.Loader): if not version.get("name") and version.get('type') == "hero_version": name_version = f"{name}_hero" else: - name_version = f"{name}_v{version:03d}" + name_version = f"{name}_v{version.get('name'):03d}" tools = unreal.AssetToolsHelpers().get_asset_tools() asset_dir, container_name = tools.create_unique_asset_name( From b376a6710445d0a2108de7244ccae37f61598fb0 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 3 Aug 2023 09:46:36 +0100 Subject: [PATCH 027/327] Added comments --- openpype/hosts/unreal/plugins/load/load_alembic_animation.py | 1 + openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py | 1 + openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py | 1 + openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py | 1 + openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py | 1 + 5 files changed, 5 insertions(+) diff --git a/openpype/hosts/unreal/plugins/load/load_alembic_animation.py b/openpype/hosts/unreal/plugins/load/load_alembic_animation.py index 059c1515c0..1d60b63f9a 100644 --- a/openpype/hosts/unreal/plugins/load/load_alembic_animation.py +++ b/openpype/hosts/unreal/plugins/load/load_alembic_animation.py @@ -77,6 +77,7 @@ class AnimationAlembicLoader(plugin.Loader): else: asset_name = "{}".format(name) version = context.get('version') + # Check if version is hero version and use different name if not version.get("name") and version.get('type') == "hero_version": name_version = f"{name}_hero" else: diff --git a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py index 8848722bd7..9285602b64 100644 --- a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py +++ b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py @@ -79,6 +79,7 @@ class SkeletalMeshAlembicLoader(plugin.Loader): else: asset_name = "{}".format(name) version = context.get('version') + # Check if version is hero version and use different name if not version.get("name") and version.get('type') == "hero_version": name_version = f"{name}_hero" else: diff --git a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py index 6fb3476d89..9aa0e4d1a8 100644 --- a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py +++ b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py @@ -53,6 +53,7 @@ class SkeletalMeshFBXLoader(plugin.Loader): else: asset_name = "{}".format(name) version = context.get('version') + # Check if version is hero version and use different name if not version.get("name") and version.get('type') == "hero_version": name_version = f"{name}_hero" else: diff --git a/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py b/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py index 20d9a31e03..bb13692f9e 100644 --- a/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py +++ b/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py @@ -81,6 +81,7 @@ class StaticMeshAlembicLoader(plugin.Loader): suffix = "_CON" asset_name = f"{asset}_{name}" if asset else f"{name}" version = context.get('version') + # Check if version is hero version and use different name if not version.get("name") and version.get('type') == "hero_version": name_version = f"{name}_hero" else: diff --git a/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py b/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py index 981003ece2..ffc68d8375 100644 --- a/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py +++ b/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py @@ -79,6 +79,7 @@ class StaticMeshFBXLoader(plugin.Loader): else: asset_name = "{}".format(name) version = context.get('version') + # Check if version is hero version and use different name if not version.get("name") and version.get('type') == "hero_version": name_version = f"{name}_hero" else: From 9d1b8c6af9e721ae3703f8b11ce2cb2c3fe3c4f4 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 3 Aug 2023 19:34:36 +0800 Subject: [PATCH 028/327] use alembic object to store the OP parameters --- openpype/hosts/max/plugins/load/load_model.py | 4 +++- openpype/hosts/max/plugins/load/load_pointcache.py | 8 +++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_model.py b/openpype/hosts/max/plugins/load/load_model.py index 810fc65968..efd758063d 100644 --- a/openpype/hosts/max/plugins/load/load_model.py +++ b/openpype/hosts/max/plugins/load/load_model.py @@ -61,6 +61,7 @@ class ModelAbcLoader(load.LoaderPlugin): rt.Select(node.Children) nodes_list = [] + abc_object = None with maintained_selection(): rt.Select(node) @@ -69,6 +70,7 @@ class ModelAbcLoader(load.LoaderPlugin): rt.Select(abc.Children) for abc_con in rt.Selection: container = rt.GetNodeByName(abc_con.name) + abc_object = container container.source = path rt.Select(container.Children) for abc_obj in rt.Selection: @@ -77,7 +79,7 @@ class ModelAbcLoader(load.LoaderPlugin): nodes_list.append(alembic_obj) abc_selections = [abc for abc in nodes_list if abc.name != "Alembic"] - load_OpenpypeData(node, abc_selections) + load_OpenpypeData(abc_object, abc_selections) lib.imprint( container["instance_node"], diff --git a/openpype/hosts/max/plugins/load/load_pointcache.py b/openpype/hosts/max/plugins/load/load_pointcache.py index 026938feff..7af588566e 100644 --- a/openpype/hosts/max/plugins/load/load_pointcache.py +++ b/openpype/hosts/max/plugins/load/load_pointcache.py @@ -67,15 +67,12 @@ class AbcLoader(load.LoaderPlugin): path = get_representation_path(representation) node = rt.GetNodeByName(container["instance_node"]) - alembic_objects = self.get_container_children(node, "AlembicObject") - for alembic_object in alembic_objects: - alembic_object.source = path - lib.imprint( container["instance_node"], {"representation": str(representation["_id"])}, ) nodes_list = [] + abc_object = None with maintained_selection(): rt.Select(node.Children) @@ -84,6 +81,7 @@ class AbcLoader(load.LoaderPlugin): rt.Select(abc.Children) for abc_con in rt.Selection: container = rt.GetNodeByName(abc_con.name) + abc_object = container container.source = path rt.Select(container.Children) for abc_obj in rt.Selection: @@ -91,7 +89,7 @@ class AbcLoader(load.LoaderPlugin): alembic_obj.source = path nodes_list.append(alembic_obj) abc_selections = [abc for abc in nodes_list if abc.name != "Alembic"] - load_OpenpypeData(node, abc_selections) + load_OpenpypeData(abc_object, abc_selections) def switch(self, container, representation): self.update(container, representation) From 697c126ccee9daa204feb2dfb01b433f24ba8064 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 4 Aug 2023 11:29:28 +0200 Subject: [PATCH 029/327] refactor expected files validator --- .../validate_expected_and_rendered_files.py | 67 ++++++++++++++----- 1 file changed, 52 insertions(+), 15 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py b/openpype/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py index ff4be677e7..9f1f7bc518 100644 --- a/openpype/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py +++ b/openpype/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py @@ -20,8 +20,19 @@ class ValidateExpectedFiles(pyblish.api.InstancePlugin): allow_user_override = True def process(self, instance): - self.instance = instance - frame_list = self._get_frame_list(instance.data["render_job_id"]) + """Process all the nodes in the instance""" + + # get dependency jobs ids for retrieving frame list + dependent_job_ids = self._get_dependent_job_ids(instance) + + if not dependent_job_ids: + self.log.warning("No dependent jobs found for instance: {}" + "".format(instance)) + return + + # get list of frames from dependent jobs + frame_list = self._get_dependent_jobs_frames( + instance, dependent_job_ids) for repre in instance.data["representations"]: expected_files = self._get_expected_files(repre) @@ -78,26 +89,45 @@ class ValidateExpectedFiles(pyblish.api.InstancePlugin): ) ) - def _get_frame_list(self, original_job_id): + def _get_dependent_job_ids(self, instance): + """Returns list of dependent job ids from instance metadata.json + + Args: + instance (pyblish.api.Instance): pyblish instance + + Returns: + (list): list of dependent job ids + + """ + dependent_job_ids = [] + + # job_id collected from metadata.json + original_job_id = instance.data["render_job_id"] + + dependent_job_ids_env = os.environ.get("RENDER_JOB_IDS") + if dependent_job_ids_env: + dependent_job_ids = dependent_job_ids_env.split(',') + elif original_job_id: + dependent_job_ids = [original_job_id] + + return dependent_job_ids + + def _get_dependent_jobs_frames(self, instance, dependent_job_ids): """Returns list of frame ranges from all render job. Render job might be re-submitted so job_id in metadata.json could be invalid. GlobalJobPreload injects current job id to RENDER_JOB_IDS. Args: - original_job_id (str) + instance (pyblish.api.Instance): pyblish instance + dependent_job_ids (list): list of dependent job ids Returns: (list) """ all_frame_lists = [] - render_job_ids = os.environ.get("RENDER_JOB_IDS") - if render_job_ids: - render_job_ids = render_job_ids.split(',') - else: # fallback - render_job_ids = [original_job_id] - for job_id in render_job_ids: - job_info = self._get_job_info(job_id) + for job_id in dependent_job_ids: + job_info = self._get_job_info(instance, job_id) frame_list = job_info["Props"].get("Frames") if frame_list: all_frame_lists.extend(frame_list.split(',')) @@ -152,18 +182,25 @@ class ValidateExpectedFiles(pyblish.api.InstancePlugin): return file_name_template, frame_placeholder - def _get_job_info(self, job_id): + def _get_job_info(self, instance, job_id): """Calls DL for actual job info for 'job_id' Might be different than job info saved in metadata.json if user manually changes job pre/during rendering. + Args: + instance (pyblish.api.Instance): pyblish instance + job_id (str): Deadline job id + + Returns: + (dict): Job info from Deadline + """ # get default deadline webservice url from deadline module - deadline_url = self.instance.context.data["defaultDeadline"] + deadline_url = instance.context.data["defaultDeadline"] # if custom one is set in instance, use that - if self.instance.data.get("deadlineUrl"): - deadline_url = self.instance.data.get("deadlineUrl") + if instance.data.get("deadlineUrl"): + deadline_url = instance.data.get("deadlineUrl") assert deadline_url, "Requires Deadline Webservice URL" url = "{}/api/jobs?JobID={}".format(deadline_url, job_id) From ff77dc067834197e70e53ee6e56da2b2ee93a103 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 4 Aug 2023 11:49:53 +0200 Subject: [PATCH 030/327] renaming target and its label to make it better sorted in ui --- openpype/hosts/nuke/api/plugin.py | 2 +- openpype/hosts/nuke/plugins/publish/collect_writes.py | 4 ++-- .../modules/deadline/plugins/publish/submit_publish_job.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index 1843134b9e..6d48c09d60 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -327,8 +327,8 @@ class NukeWriteCreator(NukeCreator): "frames": "Use existing frames" } if ("farm_rendering" in self.instance_attributes): + rendering_targets["frames_farm"] = "Use existing frames - farm" rendering_targets["farm"] = "Farm rendering" - rendering_targets["farm_frames"] = "Existing frames farm rendering" return EnumDef( "render_target", diff --git a/openpype/hosts/nuke/plugins/publish/collect_writes.py b/openpype/hosts/nuke/plugins/publish/collect_writes.py index 53827b19e9..0d552b7381 100644 --- a/openpype/hosts/nuke/plugins/publish/collect_writes.py +++ b/openpype/hosts/nuke/plugins/publish/collect_writes.py @@ -76,7 +76,7 @@ class CollectNukeWrites(pyblish.api.InstancePlugin, self.log.debug('output dir: {}'.format(output_dir)) - if render_target in ["frames", "farm_frames"]: + if render_target in ["frames", "frames_farm"]: representation = { 'name': ext, 'ext': ext, @@ -145,7 +145,7 @@ class CollectNukeWrites(pyblish.api.InstancePlugin, instance.data["representations"].append(representation) self.log.info("Publishing rendered frames ...") - if render_target == "farm_frames": + if render_target == "frames_farm": # Farm rendering instance.data["toBeRenderedOn"] = "deadline" instance.data["transfer"] = False diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 62ac36ecf1..b326f181dc 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -97,8 +97,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, hosts = ["fusion", "max", "maya", "nuke", "houdini", "celaction", "aftereffects", "harmony"] - families = ["render.farm", "render.farm_frames", - "prerender.farm", "prerender.farm_frames", + families = ["render.farm", "render.frames_farm", + "prerender.farm", "prerender.frames_farm", "renderlayer", "imagesequence", "vrayscene", "maxrender", "arnold_rop", "mantra_rop", From 9b2d14921fc32abfb90b103822f38e65f280b18d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 4 Aug 2023 20:23:55 +0800 Subject: [PATCH 031/327] fixing the bug of the handle being undefined when the linked objects to OP Parameter is deleted. --- openpype/hosts/max/api/plugin.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/hosts/max/api/plugin.py b/openpype/hosts/max/api/plugin.py index d8db716e6d..c3e7fd52b6 100644 --- a/openpype/hosts/max/api/plugin.py +++ b/openpype/hosts/max/api/plugin.py @@ -136,6 +136,7 @@ MS_CUSTOM_ATTRIB = """attributes "openPypeData" temp_arr = #() for x in all_handles do ( + if x.node == undefined do continue handle_name = node_to_name x.node append temp_arr handle_name ) @@ -209,13 +210,17 @@ class MaxCreator(Creator, MaxCreatorBase): if pre_create_data.get("use_selection"): node_list = [] + sel_list = [] for i in self.selected_nodes: node_ref = rt.NodeTransformMonitor(node=i) node_list.append(node_ref) + sel_list.append(str(i)) # Setting the property rt.setProperty( instance_node.openPypeData, "all_handles", node_list) + rt.setProperty( + instance_node.openPypeData, "sel_list", sel_list) self._add_instance_to_context(instance) imprint(instance_node.name, instance.data_to_store()) From a39626d71040afb30182a3c5c0e45e5c931bb49c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 4 Aug 2023 14:57:20 +0200 Subject: [PATCH 032/327] refactor collect_writes - also update exctract slates so it expect updated data - and move some data to collect instance data - also renaming collect instance data to host related name --- ..._data.py => collect_nuke_instance_data.py} | 17 +- .../nuke/plugins/publish/collect_writes.py | 412 ++++++++++++------ .../plugins/publish/extract_slate_frame.py | 2 +- 3 files changed, 297 insertions(+), 134 deletions(-) rename openpype/hosts/nuke/plugins/publish/{collect_instance_data.py => collect_nuke_instance_data.py} (71%) diff --git a/openpype/hosts/nuke/plugins/publish/collect_instance_data.py b/openpype/hosts/nuke/plugins/publish/collect_nuke_instance_data.py similarity index 71% rename from openpype/hosts/nuke/plugins/publish/collect_instance_data.py rename to openpype/hosts/nuke/plugins/publish/collect_nuke_instance_data.py index 3908aef4bc..ffcf49d697 100644 --- a/openpype/hosts/nuke/plugins/publish/collect_instance_data.py +++ b/openpype/hosts/nuke/plugins/publish/collect_nuke_instance_data.py @@ -2,11 +2,13 @@ import nuke import pyblish.api -class CollectInstanceData(pyblish.api.InstancePlugin): - """Collect all nodes with Avalon knob.""" +class CollectNukeInstanceData(pyblish.api.InstancePlugin): + """Collect Nuke instance data + + """ order = pyblish.api.CollectorOrder - 0.49 - label = "Collect Instance Data" + label = "Collect Nuke Instance Data" hosts = ["nuke", "nukeassist"] # presets @@ -40,5 +42,14 @@ class CollectInstanceData(pyblish.api.InstancePlugin): "pixelAspect": pixel_aspect }) + + # add review family if review activated on instance + if instance.data.get("review"): + instance.data["families"].append("review") + + # add creator attributes to instance + creator_attributes = instance.data["creator_attributes"] + instance.data.update(creator_attributes) + self.log.debug("Collected instance: {}".format( instance.data)) diff --git a/openpype/hosts/nuke/plugins/publish/collect_writes.py b/openpype/hosts/nuke/plugins/publish/collect_writes.py index 0d552b7381..307d323908 100644 --- a/openpype/hosts/nuke/plugins/publish/collect_writes.py +++ b/openpype/hosts/nuke/plugins/publish/collect_writes.py @@ -1,5 +1,4 @@ import os -from pprint import pformat import nuke import pyblish.api from openpype.hosts.nuke import api as napi @@ -15,33 +14,16 @@ class CollectNukeWrites(pyblish.api.InstancePlugin, hosts = ["nuke", "nukeassist"] families = ["render", "prerender", "image"] + # cashing + _write_nodes = {} + _frame_ranges = {} + def process(self, instance): - self.log.debug(pformat(instance.data)) - creator_attributes = instance.data["creator_attributes"] - instance.data.update(creator_attributes) group_node = instance.data["transientData"]["node"] render_target = instance.data["render_target"] - family = instance.data["family"] - families = instance.data["families"] - # add targeted family to families - instance.data["families"].append( - "{}.{}".format(family, render_target) - ) - self.log.debug("Appending render target to families: {}.{}".format( - family, render_target) - ) - if instance.data.get("review"): - instance.data["families"].append("review") - - child_nodes = napi.get_instance_group_node_childs(instance) - instance.data["transientData"]["childNodes"] = child_nodes - - write_node = None - for x in child_nodes: - if x.Class() == "Write": - write_node = x + write_node = self._write_node_helper(instance) if write_node is None: self.log.warning( @@ -51,131 +33,134 @@ class CollectNukeWrites(pyblish.api.InstancePlugin, ) return - instance.data["writeNode"] = write_node - self.log.debug("checking instance: {}".format(instance)) + # get colorspace and add to version data + colorspace = napi.get_colorspace_from_node(write_node) - # Determine defined file type - ext = write_node["file_type"].value() + if render_target == "frames": + self._set_existing_files_data(instance, colorspace) - # Get frame range - handle_start = instance.context.data["handleStart"] - handle_end = instance.context.data["handleEnd"] - first_frame = int(nuke.root()["first_frame"].getValue()) - last_frame = int(nuke.root()["last_frame"].getValue()) - frame_length = int(last_frame - first_frame + 1) + elif render_target == "frames_farm": + collected_frames = self._set_existing_files_data( + instance, colorspace) - if write_node["use_limit"].getValue(): - first_frame = int(write_node["first"].getValue()) - last_frame = int(write_node["last"].getValue()) + self._set_expected_files(instance, collected_frames) + + self._add_farm_instance_data(instance) + + elif render_target == "farm": + self._add_farm_instance_data(instance) + + # set additional instance data + self._set_additional_instance_data(instance, render_target, colorspace) + + def _set_existing_files_data(self, instance, colorspace): + """Set existing files data to instance data. + + Args: + instance (pyblish.api.Instance): pyblish instance + colorspace (str): colorspace + + Returns: + list: collected frames + """ + collected_frames = self._get_collected_frames(instance) + + representation = self._get_existing_frames_representation( + instance, collected_frames + ) + + # inject colorspace data + self.set_representation_colorspace( + representation, instance.context, + colorspace=colorspace + ) + + instance.data["representations"].append(representation) + + return collected_frames + + def _set_expected_files(self, instance, collected_frames): + """Set expected files to instance data. + + Args: + instance (pyblish.api.Instance): pyblish instance + collected_frames (list): collected frames + """ + write_node = self._write_node_helper(instance) write_file_path = nuke.filename(write_node) output_dir = os.path.dirname(write_file_path) - # get colorspace and add to version data - colorspace = napi.get_colorspace_from_node(write_node) + instance.data["expectedFiles"] = [ + os.path.join(output_dir, source_file) + for source_file in collected_frames + ] - self.log.debug('output dir: {}'.format(output_dir)) + def _get_frame_range_data(self, instance): + """Get frame range data from instance. - if render_target in ["frames", "frames_farm"]: - representation = { - 'name': ext, - 'ext': ext, - "stagingDir": output_dir, - "tags": [] - } + Args: + instance (pyblish.api.Instance): pyblish instance - # get file path knob - node_file_knob = write_node["file"] - # list file paths based on input frames - expected_paths = list(sorted({ - node_file_knob.evaluate(frame) - for frame in range(first_frame, last_frame + 1) - })) + Returns: + tuple: first_frame, last_frame + """ - # convert only to base names - expected_filenames = [ - os.path.basename(filepath) - for filepath in expected_paths - ] + instance_name = instance.data["name"] - # make sure files are existing at folder - collected_frames = [ - filename - for filename in os.listdir(output_dir) - if filename in expected_filenames - ] + if self._frame_ranges.get(instance_name): + # return cashed write node + return self._frame_ranges[instance_name] - if collected_frames: - collected_frames_len = len(collected_frames) - frame_start_str = "%0{}d".format( - len(str(last_frame))) % first_frame - representation['frameStart'] = frame_start_str + write_node = self._write_node_helper(instance) - # in case slate is expected and not yet rendered - self.log.debug("_ frame_length: {}".format(frame_length)) - self.log.debug("_ collected_frames_len: {}".format( - collected_frames_len)) + # Get frame range from workfile + first_frame = int(nuke.root()["first_frame"].getValue()) + last_frame = int(nuke.root()["last_frame"].getValue()) - # this will only run if slate frame is not already - # rendered from previews publishes - if ( - "slate" in families - and frame_length == collected_frames_len - and family == "render" - ): - frame_slate_str = ( - "{{:0{}d}}".format(len(str(last_frame))) - ).format(first_frame - 1) + # Get frame range from write node if activated + if write_node["use_limit"].getValue(): + first_frame = int(write_node["first"].getValue()) + last_frame = int(write_node["last"].getValue()) - slate_frame = collected_frames[0].replace( - frame_start_str, frame_slate_str) - collected_frames.insert(0, slate_frame) + # add to cache + self._frame_ranges[instance_name] = (first_frame, last_frame) - if collected_frames_len == 1: - representation['files'] = collected_frames.pop() - else: - representation['files'] = collected_frames + return first_frame, last_frame - # inject colorspace data - self.set_representation_colorspace( - representation, instance.context, - colorspace=colorspace - ) + def _set_additional_instance_data( + self, instance, render_target, colorspace + ): + """Set additional instance data. - instance.data["representations"].append(representation) - self.log.info("Publishing rendered frames ...") + Args: + instance (pyblish.api.Instance): pyblish instance + render_target (str): render target + colorspace (str): colorspace + """ + family = instance.data["family"] - if render_target == "frames_farm": - # Farm rendering - instance.data["toBeRenderedOn"] = "deadline" - instance.data["transfer"] = False - instance.data["farm"] = True # to skip integrate - self.log.info("Farm rendering ON ...") + # add targeted family to families + instance.data["families"].append( + "{}.{}".format(family, render_target) + ) + self.log.info("Appending render target to families: {}.{}".format( + family, render_target) + ) - self.log.info( - "Adding collected files %s to expectedFiles instance.data", - collected_frames - ) - if "expectedFiles" not in instance.data: - instance.data["expectedFiles"] = list() - for source_file in collected_frames: - instance.data["expectedFiles"].append( - os.path.join(output_dir, source_file) - ) + write_node = self._write_node_helper(instance) - elif render_target == "farm": - farm_keys = ["farm_chunk", "farm_priority", "farm_concurrency"] - for key in farm_keys: - # Skip if key is not in creator attributes - if key not in creator_attributes: - continue - # Add farm attributes to instance - instance.data[key] = creator_attributes[key] + # Determine defined file type + ext = write_node["file_type"].value() - # Farm rendering - instance.data["transfer"] = False - instance.data["farm"] = True - self.log.info("Farm rendering ON ...") + # get frame range data + handle_start = instance.context.data["handleStart"] + handle_end = instance.context.data["handleEnd"] + first_frame, last_frame = self._get_frame_range_data(instance) + + # get output paths + write_file_path = nuke.filename(write_node) + output_dir = os.path.dirname(write_file_path) # TODO: remove this when we have proper colorspace support version_data = { @@ -209,10 +194,6 @@ class CollectNukeWrites(pyblish.api.InstancePlugin, "frameEndHandle": last_frame, }) - # make sure rendered sequence on farm will - # be used for extract review - if not instance.data.get("review"): - instance.data["useSequenceForReview"] = False # TODO temporarily set stagingDir as persistent for backward # compatibility. This is mainly focused on `renders`folders which @@ -220,4 +201,175 @@ class CollectNukeWrites(pyblish.api.InstancePlugin, # this logic should be removed and replaced with custom staging dir instance.data["stagingDir_persistent"] = True - self.log.debug("instance.data: {}".format(pformat(instance.data))) + def _write_node_helper(self, instance): + """Helper function to get write node from instance. + + Also sets instance transient data with child nodes. + + Args: + instance (pyblish.api.Instance): pyblish instance + + Returns: + nuke.Node: write node + """ + instance_name = instance.data["name"] + + if self._write_nodes.get(instance_name): + # return cashed write node + return self._write_nodes[instance_name] + + # get all child nodes from group node + child_nodes = napi.get_instance_group_node_childs(instance) + + # set child nodes to instance transient data + instance.data["transientData"]["childNodes"] = child_nodes + + write_node = None + for node_ in child_nodes: + if node_.Class() == "Write": + write_node = node_ + + if write_node: + # for slate frame extraction + instance.data["transientData"]["writeNode"] = write_node + # add to cache + self._write_nodes[instance_name] = write_node + + return self._write_nodes[instance_name] + + def _get_existing_frames_representation( + self, + instance, + collected_frames + ): + """Get existing frames representation. + + Args: + instance (pyblish.api.Instance): pyblish instance + collected_frames (list): collected frames + + Returns: + dict: representation + """ + + first_frame, last_frame = self._get_frame_range_data(instance) + + write_node = self._write_node_helper(instance) + + write_file_path = nuke.filename(write_node) + output_dir = os.path.dirname(write_file_path) + + # Determine defined file type + ext = write_node["file_type"].value() + + representation = { + "name": ext, + "ext": ext, + "stagingDir": output_dir, + "tags": [] + } + + frame_start_str = "%0{}d".format( + len(str(last_frame))) % first_frame + representation['frameStart'] = frame_start_str + + # set slate frame + collected_frames = self._add_slate_frame_to_collected_frames( + instance, + collected_frames, + frame_start_str, + first_frame, + last_frame + ) + + if len(collected_frames) == 1: + representation['files'] = collected_frames.pop() + else: + representation['files'] = collected_frames + + return representation + + def _add_slate_frame_to_collected_frames( + self, + instance, + collected_frames, + frame_start_str, + first_frame, + last_frame + ): + """Set slate frame.""" + frame_length = int(last_frame - first_frame + 1) + + # this will only run if slate frame is not already + # rendered from previews publishes + if ( + "slate" in instance.data["families"] + and frame_length == len(collected_frames) + and instance.data["family"] == "render" + ): + frame_slate_str = ( + "{{:0{}d}}".format(len(str(last_frame))) + ).format(first_frame - 1) + + slate_frame = collected_frames[0].replace( + frame_start_str, frame_slate_str) + collected_frames.insert(0, slate_frame) + + return collected_frames + + def _add_farm_instance_data(self, instance): + """Add farm publishing related instance data. + + Args: + instance (pyblish.api.Instance): pyblish instance + """ + + # make sure rendered sequence on farm will + # be used for extract review + if not instance.data.get("review"): + instance.data["useSequenceForReview"] = False + + # Farm rendering + instance.data["transfer"] = False + instance.data["farm"] = True + self.log.info("Farm rendering ON ...") + + def _get_collected_frames(self, instance): + """Get collected frames. + + Args: + instance (pyblish.api.Instance): pyblish instance + + Returns: + list: collected frames + """ + + first_frame, last_frame = self._get_frame_range_data(instance) + + write_node = self._write_node_helper(instance) + + write_file_path = nuke.filename(write_node) + output_dir = os.path.dirname(write_file_path) + + # get file path knob + node_file_knob = write_node["file"] + # list file paths based on input frames + expected_paths = list(sorted({ + node_file_knob.evaluate(frame) + for frame in range(first_frame, last_frame + 1) + })) + + # convert only to base names + expected_filenames = [ + os.path.basename(filepath) + for filepath in expected_paths + ] + + # make sure files are existing at folder + collected_frames = [ + filename + for filename in os.listdir(output_dir) + if filename in expected_filenames + ] + + return collected_frames diff --git a/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py b/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py index 06c086b10d..25262a7418 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py +++ b/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py @@ -249,7 +249,7 @@ class ExtractSlateFrame(publish.Extractor): # Add file to representation files # - get write node - write_node = instance.data["writeNode"] + write_node = instance.data["transientData"]["writeNode"] # - evaluate filepaths for first frame and slate frame first_filename = os.path.basename( write_node["file"].evaluate(first_frame)) From 8fc7d6c57d65e601fd2effa31695b03a53e84373 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 4 Aug 2023 14:59:40 +0200 Subject: [PATCH 033/327] pr suggestions --- openpype/hosts/nuke/plugins/publish/collect_writes.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/collect_writes.py b/openpype/hosts/nuke/plugins/publish/collect_writes.py index 307d323908..fce2e571a0 100644 --- a/openpype/hosts/nuke/plugins/publish/collect_writes.py +++ b/openpype/hosts/nuke/plugins/publish/collect_writes.py @@ -330,8 +330,10 @@ class CollectNukeWrites(pyblish.api.InstancePlugin, instance.data["useSequenceForReview"] = False # Farm rendering - instance.data["transfer"] = False - instance.data["farm"] = True + instance.data.update({ + "transfer": False, + "farm": True # to skip integrate + }) self.log.info("Farm rendering ON ...") def _get_collected_frames(self, instance): From 9e49a812933c4bccc885c1978f8112143a8ba3ce Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 4 Aug 2023 15:15:50 +0200 Subject: [PATCH 034/327] double negative comment --- openpype/hosts/nuke/api/lib.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 83bb04c64d..e57608e1e1 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2105,23 +2105,29 @@ class WorkfileSettings(object): config_data["path"])) # check if there's a mismatch between environment and settings - wrong_environment = self._is_settings_different_from_environment( + correct_settings = self._is_settings_matching_environment( config_data) # if there's no mismatch between environment and settings - if not wrong_environment: + if correct_settings: self._set_ocio_config_path_to_workfile(config_data) - def _is_settings_different_from_environment(self, config_data): + def _is_settings_matching_environment(self, config_data): """ Check if OCIO config path is different from environment Args: config_data (dict): OCIO config data from settings Returns: - bool: True if there's a mismatch between environment and settings + bool: True if settings are matching environment, False otherwise """ - current_ocio_path = os.getenv("OCIO") + current_ocio_path = os.environ["OCIO"] + settings_ocio_path = config_data["path"] + + # normalize all paths to forward slashes + current_ocio_path = current_ocio_path.replace("\\", "/") + settings_ocio_path = settings_ocio_path.replace("\\", "/") + if current_ocio_path != config_data["path"]: message = """ It seems like there's a mismatch between the OCIO config path set in your Nuke @@ -2147,7 +2153,9 @@ Reopening Nuke should synchronize these paths and resolve any discrepancies. settings_path=config_data["path"] ) ) - return True + return False + + return True def _set_ocio_config_path_to_workfile(self, config_data): """ Set OCIO config path to workfile From 8c0b6dc252d97968cf091bb71399cd84c63b3798 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 4 Aug 2023 16:02:52 +0200 Subject: [PATCH 035/327] accepting environment vars used in config template --- openpype/hosts/nuke/api/lib.py | 56 ++++++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index e57608e1e1..00ce94eccc 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2170,9 +2170,8 @@ Reopening Nuke should synchronize these paths and resolve any discrepancies. """ # replace path with env var if possible - ocio_path = self._replace_ocio_path_with_env_var( - config_data["path"] - ) + ocio_path = self._replace_ocio_path_with_env_var(config_data) + log.info("Setting OCIO config path to: `{}`".format( ocio_path)) @@ -2186,7 +2185,35 @@ Reopening Nuke should synchronize these paths and resolve any discrepancies. log.info("Saving script to apply OCIO config path change.") nuke.scriptSave() - def _replace_ocio_path_with_env_var(self, path): + def _get_included_vars(self, config_template): + """ Get all environment variables included in template + + Args: + config_template (str): OCIO config template from settings + + Returns: + list: list of environment variables included in template + """ + # resolve all environments for whitelist variables + included_vars = [ + "BUILTIN_OCIO_ROOT", + ] + + # include all project root related env vars + for env_var in os.environ: + if env_var.startswith("OPENPYPE_PROJECT_ROOT_"): + included_vars.append(env_var) + + # use regex to find env var in template with format {ENV_VAR} + # this way we make sure only template used env vars are included + env_var_regex = r"\{([A-Z_]+)\}" + env_var = re.findall(env_var_regex, config_template) + if env_var: + included_vars.append(env_var[0]) + + return included_vars + + def _replace_ocio_path_with_env_var(self, config_data): """ Replace OCIO config path with environment variable Environment variable is added as TCL expression to path. TCL expression @@ -2194,19 +2221,22 @@ Reopening Nuke should synchronize these paths and resolve any discrepancies. formatted values. Args: - path (str): OCIO config path + config_data (str): OCIO config dict from settings Returns: - str: OCIO config path with environment variable + str: OCIO config path with environment variable TCL expression """ - # QUESTION: should we also include other names variants - included_vars = [ - "BUILTIN_OCIO_ROOT", - "OPENPYPE_PROJECT_ROOT" - ] + config_path = config_data["path"] + config_template = config_data["template"] + + included_vars = self._get_included_vars(config_template) + + # make sure we return original path if no env var is included + new_path = config_path + for env_var, env_path in os.environ.items(): # first check if variable is whitelisted - if all(var_ not in env_var for var_ in included_vars): + if env_var not in included_vars: # included vars not found in env_var name continue @@ -2216,7 +2246,7 @@ Reopening Nuke should synchronize these paths and resolve any discrepancies. # make sure paths are in same format env_path = env_path.replace("\\", "/") - path = path.replace("\\", "/") + path = config_path.replace("\\", "/") # check if env_path is in path and replace to first found positive if env_path in path: From 85a9e5a5682e07f37b9537e0bddfed52fc580d89 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 7 Aug 2023 15:42:48 +0800 Subject: [PATCH 036/327] hound shut --- openpype/hosts/max/api/plugin.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/api/plugin.py b/openpype/hosts/max/api/plugin.py index c026b2ef6f..3389447cb0 100644 --- a/openpype/hosts/max/api/plugin.py +++ b/openpype/hosts/max/api/plugin.py @@ -221,9 +221,11 @@ class MaxCreator(Creator, MaxCreatorBase): # Setting the property rt.setProperty( - instance_node.modifiers[0].openPypeData, "all_handles", node_list) + instance_node.modifiers[0].openPypeData, + "all_handles", node_list) rt.setProperty( - instance_node.modifiers[0].openPypeData, "sel_list", sel_list) + instance_node.modifiers[0].openPypeData, + "sel_list", sel_list) self._add_instance_to_context(instance) imprint(instance_node.name, instance.data_to_store()) From 5b68f0cef6fc72b0a7e011e31acfc9bd6b7b2c9d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 7 Aug 2023 16:32:34 +0800 Subject: [PATCH 037/327] remove loading openpype attributes in load max scene as it is being loaded differently --- openpype/hosts/max/plugins/load/load_max_scene.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_max_scene.py b/openpype/hosts/max/plugins/load/load_max_scene.py index 468461bc0e..f161a19a4c 100644 --- a/openpype/hosts/max/plugins/load/load_max_scene.py +++ b/openpype/hosts/max/plugins/load/load_max_scene.py @@ -1,7 +1,7 @@ import os from openpype.hosts.max.api import lib -from openpype.hosts.max.api.pipeline import containerise, load_OpenpypeData +from openpype.hosts.max.api.pipeline import containerise from openpype.pipeline import get_representation_path, load @@ -28,7 +28,6 @@ class MaxSceneLoader(load.LoaderPlugin): rt.MergeMaxFile(path) max_objects = rt.getLastMergedNodes() max_container = rt.Container(name=f"{name}") - load_OpenpypeData(max_container, max_objects) for max_object in max_objects: max_object.Parent = max_container @@ -50,7 +49,6 @@ class MaxSceneLoader(load.LoaderPlugin): for max_object in max_objects: max_object.Parent = instance_container instance_container.Parent = container_node - load_OpenpypeData(container_node, max_objects) lib.imprint(container["instance_node"], { "representation": str(representation["_id"]) }) From 69f88b33d2ce3a96463105e7e3ff7e4066509bbc Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 7 Aug 2023 18:17:10 +0800 Subject: [PATCH 038/327] rstore max scene code --- openpype/hosts/max/plugins/load/load_max_scene.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/load/load_max_scene.py b/openpype/hosts/max/plugins/load/load_max_scene.py index f161a19a4c..468461bc0e 100644 --- a/openpype/hosts/max/plugins/load/load_max_scene.py +++ b/openpype/hosts/max/plugins/load/load_max_scene.py @@ -1,7 +1,7 @@ import os from openpype.hosts.max.api import lib -from openpype.hosts.max.api.pipeline import containerise +from openpype.hosts.max.api.pipeline import containerise, load_OpenpypeData from openpype.pipeline import get_representation_path, load @@ -28,6 +28,7 @@ class MaxSceneLoader(load.LoaderPlugin): rt.MergeMaxFile(path) max_objects = rt.getLastMergedNodes() max_container = rt.Container(name=f"{name}") + load_OpenpypeData(max_container, max_objects) for max_object in max_objects: max_object.Parent = max_container @@ -49,6 +50,7 @@ class MaxSceneLoader(load.LoaderPlugin): for max_object in max_objects: max_object.Parent = instance_container instance_container.Parent = container_node + load_OpenpypeData(container_node, max_objects) lib.imprint(container["instance_node"], { "representation": str(representation["_id"]) }) From 4af2ddaf49902050faee13ed4e5b5653a0e48782 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 7 Aug 2023 17:22:50 +0200 Subject: [PATCH 039/327] nuke: nicer error communication to users. --- openpype/hosts/nuke/api/lib.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 21036e5b11..5ea6752579 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2379,25 +2379,24 @@ Reopening Nuke should synchronize these paths and resolve any discrepancies. knobs["to"])) def set_colorspace(self): - ''' Setting colorpace following presets + ''' Setting colorspace following presets ''' # get imageio nuke_colorspace = get_nuke_imageio_settings() log.info("Setting colorspace to workfile...") - # try: - self.set_root_colorspace(nuke_colorspace) - # except AttributeError: - # msg = "set_colorspace(): missing `workfile` settings in template" - # nuke.message(msg) + try: + self.set_root_colorspace(nuke_colorspace) + except AttributeError as _error: + msg = "Set Colorspace to workfile error: {}".format(_error) + nuke.message(msg) log.info("Setting colorspace to viewers...") try: self.set_viewers_colorspace(nuke_colorspace["viewer"]) - except AttributeError: - msg = "set_colorspace(): missing `viewer` settings in template" + except AttributeError as _error: + msg = "Set Colorspace to viewer error: {}".format(_error) nuke.message(msg) - log.error(msg) log.info("Setting colorspace to write nodes...") try: From e35c9898af6716703f5d468f3e7490cdc7ddecc9 Mon Sep 17 00:00:00 2001 From: Fabia Serra Arrizabalaga Date: Mon, 7 Aug 2023 19:08:31 +0200 Subject: [PATCH 040/327] Align creator settings to use default_variants instead of defaults --- .../plugins/create/create_arnold_ass.py | 2 +- .../plugins/create/create_arnold_rop.py | 2 +- .../plugins/create/create_karma_rop.py | 2 +- .../plugins/create/create_mantra_rop.py | 2 +- .../plugins/create/create_redshift_rop.py | 2 +- .../houdini/plugins/create/create_vray_rop.py | 2 +- .../defaults/project_settings/houdini.json | 56 ++++- .../defaults/project_settings/maya.json | 47 ++-- .../schemas/schema_houdini_create.json | 206 ++++++++++-------- .../schemas/schema_maya_create.json | 26 ++- .../schemas/schema_maya_create_render.json | 4 +- .../schemas/template_create_plugin.json | 4 +- 12 files changed, 211 insertions(+), 144 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_arnold_ass.py b/openpype/hosts/houdini/plugins/create/create_arnold_ass.py index 8b310753d0..e587041e70 100644 --- a/openpype/hosts/houdini/plugins/create/create_arnold_ass.py +++ b/openpype/hosts/houdini/plugins/create/create_arnold_ass.py @@ -10,7 +10,7 @@ class CreateArnoldAss(plugin.HoudiniCreator): label = "Arnold ASS" family = "ass" icon = "magic" - defaults = ["Main"] + default_variants = ["Main"] # Default extension: `.ass` or `.ass.gz` ext = ".ass" diff --git a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py index ca516619f6..7d00e8b3e5 100644 --- a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py @@ -9,7 +9,7 @@ class CreateArnoldRop(plugin.HoudiniCreator): label = "Arnold ROP" family = "arnold_rop" icon = "magic" - defaults = ["master"] + default_variants = ["master"] # Default extension ext = "exr" diff --git a/openpype/hosts/houdini/plugins/create/create_karma_rop.py b/openpype/hosts/houdini/plugins/create/create_karma_rop.py index 71c2bf1b28..e8b77c12e5 100644 --- a/openpype/hosts/houdini/plugins/create/create_karma_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_karma_rop.py @@ -11,7 +11,7 @@ class CreateKarmaROP(plugin.HoudiniCreator): label = "Karma ROP" family = "karma_rop" icon = "magic" - defaults = ["master"] + default_variants = ["master"] def create(self, subset_name, instance_data, pre_create_data): import hou # noqa diff --git a/openpype/hosts/houdini/plugins/create/create_mantra_rop.py b/openpype/hosts/houdini/plugins/create/create_mantra_rop.py index 5c29adb33f..b2846d53fa 100644 --- a/openpype/hosts/houdini/plugins/create/create_mantra_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_mantra_rop.py @@ -11,7 +11,7 @@ class CreateMantraROP(plugin.HoudiniCreator): label = "Mantra ROP" family = "mantra_rop" icon = "magic" - defaults = ["master"] + default_variants = ["master"] def create(self, subset_name, instance_data, pre_create_data): import hou # noqa diff --git a/openpype/hosts/houdini/plugins/create/create_redshift_rop.py b/openpype/hosts/houdini/plugins/create/create_redshift_rop.py index 8f4aa1327d..9b0a36ddd1 100644 --- a/openpype/hosts/houdini/plugins/create/create_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_redshift_rop.py @@ -13,7 +13,7 @@ class CreateRedshiftROP(plugin.HoudiniCreator): label = "Redshift ROP" family = "redshift_rop" icon = "magic" - defaults = ["master"] + default_variants = ["master"] ext = "exr" def create(self, subset_name, instance_data, pre_create_data): diff --git a/openpype/hosts/houdini/plugins/create/create_vray_rop.py b/openpype/hosts/houdini/plugins/create/create_vray_rop.py index 58748d4c34..a3ae01f5ae 100644 --- a/openpype/hosts/houdini/plugins/create/create_vray_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_vray_rop.py @@ -14,7 +14,7 @@ class CreateVrayROP(plugin.HoudiniCreator): label = "VRay ROP" family = "vray_rop" icon = "magic" - defaults = ["master"] + default_variants = ["master"] ext = "exr" diff --git a/openpype/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json index a53f1ff202..389fdc6d96 100644 --- a/openpype/settings/defaults/project_settings/houdini.json +++ b/openpype/settings/defaults/project_settings/houdini.json @@ -14,48 +14,80 @@ "create": { "CreateArnoldAss": { "enabled": true, - "defaults": [], + "default_variants": [], "ext": ".ass" }, + "CreateArnoldRop": { + "enabled": true, + "default_variants": [] + }, "CreateAlembicCamera": { "enabled": true, - "defaults": [] + "default_variants": [] + }, + "CreateBGEO": { + "enabled": true, + "default_variants": [] }, "CreateCompositeSequence": { "enabled": true, - "defaults": [] + "default_variants": [] + }, + "CreateHDA": { + "enabled": true, + "default_variants": [] + }, + "CreateKarmaROP": { + "enabled": true, + "default_variants": [] + }, + "CreateMantraROP": { + "enabled": true, + "default_variants": [] }, "CreatePointCache": { "enabled": true, - "defaults": [] + "default_variants": [] + }, + "CreateRedshiftProxy": { + "enabled": true, + "default_variants": [] }, "CreateRedshiftROP": { "enabled": true, - "defaults": [] + "default_variants": [] }, "CreateRemotePublish": { "enabled": true, - "defaults": [] + "default_variants": [] }, - "CreateVDBCache": { + "CreateReview": { "enabled": true, - "defaults": [] + "default_variants": [] }, "CreateUSD": { "enabled": false, - "defaults": [] + "default_variants": [] }, "CreateUSDModel": { "enabled": false, - "defaults": [] + "default_variants": [] }, "USDCreateShadingWorkspace": { "enabled": false, - "defaults": [] + "default_variants": [] }, "CreateUSDRender": { "enabled": false, - "defaults": [] + "default_variants": [] + }, + "CreateVDBCache": { + "enabled": true, + "default_variants": [] + }, + "CreateVrayROP": { + "enabled": true, + "default_variants": [] } }, "publish": { diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 8e1022f877..b9817fe400 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -521,19 +521,19 @@ "enabled": true, "make_tx": true, "rs_tex": false, - "defaults": [ + "default_variants": [ "Main" ] }, "CreateRender": { "enabled": true, - "defaults": [ + "default_variants": [ "Main" ] }, "CreateUnrealStaticMesh": { "enabled": true, - "defaults": [ + "default_variants": [ "", "_Main" ], @@ -547,7 +547,7 @@ }, "CreateUnrealSkeletalMesh": { "enabled": true, - "defaults": [], + "default_variants": [], "joint_hints": "jnt_org" }, "CreateMultiverseLook": { @@ -555,11 +555,12 @@ "publish_mip_map": true }, "CreateAnimation": { + "enabled": false, "write_color_sets": false, "write_face_sets": false, "include_parent_hierarchy": false, "include_user_defined_attributes": false, - "defaults": [ + "default_variants": [ "Main" ] }, @@ -567,7 +568,7 @@ "enabled": true, "write_color_sets": false, "write_face_sets": false, - "defaults": [ + "default_variants": [ "Main", "Proxy", "Sculpt" @@ -578,7 +579,7 @@ "write_color_sets": false, "write_face_sets": false, "include_user_defined_attributes": false, - "defaults": [ + "default_variants": [ "Main" ] }, @@ -586,20 +587,20 @@ "enabled": true, "write_color_sets": false, "write_face_sets": false, - "defaults": [ + "default_variants": [ "Main" ] }, "CreateReview": { "enabled": true, - "defaults": [ + "default_variants": [ "Main" ], "useMayaTimeline": true }, "CreateAss": { "enabled": true, - "defaults": [ + "default_variants": [ "Main" ], "expandProcedurals": false, @@ -621,61 +622,61 @@ "enabled": true, "vrmesh": true, "alembic": true, - "defaults": [ + "default_variants": [ "Main" ] }, "CreateMultiverseUsd": { "enabled": true, - "defaults": [ + "default_variants": [ "Main" ] }, "CreateMultiverseUsdComp": { "enabled": true, - "defaults": [ + "default_variants": [ "Main" ] }, "CreateMultiverseUsdOver": { "enabled": true, - "defaults": [ + "default_variants": [ "Main" ] }, "CreateAssembly": { "enabled": true, - "defaults": [ + "default_variants": [ "Main" ] }, "CreateCamera": { "enabled": true, - "defaults": [ + "default_variants": [ "Main" ] }, "CreateLayout": { "enabled": true, - "defaults": [ + "default_variants": [ "Main" ] }, "CreateMayaScene": { "enabled": true, - "defaults": [ + "default_variants": [ "Main" ] }, "CreateRenderSetup": { "enabled": true, - "defaults": [ + "default_variants": [ "Main" ] }, "CreateRig": { "enabled": true, - "defaults": [ + "default_variants": [ "Main", "Sim", "Cloth" @@ -683,20 +684,20 @@ }, "CreateSetDress": { "enabled": true, - "defaults": [ + "default_variants": [ "Main", "Anim" ] }, "CreateVRayScene": { "enabled": true, - "defaults": [ + "default_variants": [ "Main" ] }, "CreateYetiRig": { "enabled": true, - "defaults": [ + "default_variants": [ "Main" ] } diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_create.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_create.json index 83e0cf789a..52a03677da 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_create.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_create.json @@ -1,89 +1,121 @@ { - "type": "dict", - "collapsible": true, - "key": "create", - "label": "Creator plugins", - "children": [ - { - "type": "dict", - "collapsible": true, - "key": "CreateArnoldAss", - "label": "Create Arnold Ass", - "checkbox_key": "enabled", - "children": [ - { - "type": "boolean", - "key": "enabled", - "label": "Enabled" - }, - { - "type": "list", - "key": "defaults", - "label": "Default Subsets", - "object_type": "text" - }, - { - "type": "enum", - "key": "ext", - "label": "Default Output Format (extension)", - "multiselection": false, - "enum_items": [ - { - ".ass": ".ass" - }, - { - ".ass.gz": ".ass.gz (gzipped)" - } - ] - } - ] - - }, + "type": "dict", + "collapsible": true, + "key": "create", + "label": "Creator plugins", + "children": [ { - "type": "schema_template", - "name": "template_create_plugin", - "template_data": [ - { - "key": "CreateAlembicCamera", - "label": "Create Alembic Camera" - }, - { - "key": "CreateCompositeSequence", - "label": "Create Composite (Image Sequence)" - }, - { - "key": "CreatePointCache", - "label": "Create Point Cache" - }, - { - "key": "CreateRedshiftROP", - "label": "Create Redshift ROP" - }, - { - "key": "CreateRemotePublish", - "label": "Create Remote Publish" - }, - { - "key": "CreateVDBCache", - "label": "Create VDB Cache" - }, - { - "key": "CreateUSD", - "label": "Create USD" - }, - { - "key": "CreateUSDModel", - "label": "Create USD Model" - }, - { - "key": "USDCreateShadingWorkspace", - "label": "Create USD Shading Workspace" - }, - { - "key": "CreateUSDRender", - "label": "Create USD Render" - } - ] - } - ] -} \ No newline at end of file + "type": "dict", + "collapsible": true, + "key": "CreateArnoldAss", + "label": "Create Arnold Ass", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "list", + "key": "default_variants", + "label": "Default Variants", + "object_type": "text" + }, + { + "type": "enum", + "key": "ext", + "label": "Default Output Format (extension)", + "multiselection": false, + "enum_items": [ + { + ".ass": ".ass" + }, + { + ".ass.gz": ".ass.gz (gzipped)" + } + ] + } + ] + + }, + { + "type": "schema_template", + "name": "template_create_plugin", + "template_data": [ + { + "key": "CreateArnoldRop", + "label": "Create Arnold ROP" + }, + { + "key": "CreateAlembicCamera", + "label": "Create Alembic Camera" + }, + { + "key": "CreateBGEO", + "label": "Create Houdini BGEO" + }, + { + "key": "CreateCompositeSequence", + "label": "Create Composite (Image Sequence)" + }, + { + "key": "CreateKarmaROP", + "label": "Create Karma ROP" + }, + { + "key": "CreateMantraROP", + "label": "Create Mantra ROP" + }, + { + "key": "CreateHDA", + "label": "Create HDA" + }, + { + "key": "CreatePointCache", + "label": "Create Point Cache" + }, + { + "key": "CreateRedshiftProxy", + "label": "Create Redshift Proxy" + }, + { + "key": "CreateRedshiftROP", + "label": "Create Redshift ROP" + }, + { + "key": "CreateRemotePublish", + "label": "Create Remote Publish" + }, + { + "key": "CreateReview", + "label": "Create Review" + }, + { + "key": "CreateUSD", + "label": "Create USD" + }, + { + "key": "CreateUSDModel", + "label": "Create USD Model" + }, + { + "key": "USDCreateShadingWorkspace", + "label": "Create USD Shading Workspace" + }, + { + "key": "CreateUSDRender", + "label": "Create USD Render" + }, + { + "key": "CreateVDBCache", + "label": "Create VDB Cache" + }, + { + "key": "CreateVrayROP", + "label": "Create VRay ROP" + } + ] + } + ] +} diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json index d28d42c10c..8faf3fcae8 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json @@ -28,7 +28,7 @@ }, { "type": "list", - "key": "defaults", + "key": "default_variants", "label": "Default Subsets", "object_type": "text" } @@ -52,7 +52,7 @@ }, { "type": "list", - "key": "defaults", + "key": "default_variants", "label": "Default Subsets", "object_type": "text" }, @@ -84,7 +84,7 @@ }, { "type": "list", - "key": "defaults", + "key": "default_variants", "label": "Default Subsets", "object_type": "text" }, @@ -120,10 +120,12 @@ "collapsible": true, "key": "CreateAnimation", "label": "Create Animation", + "checkbox_key": "enabled", "children": [ { - "type": "label", - "label": "This plugin is not optional due to implicit creation through loading the \"rig\" family.\nThis family is also hidden from creation due to complexity in setup." + "type": "boolean", + "key": "enabled", + "label": "Enabled" }, { "type": "boolean", @@ -147,7 +149,7 @@ }, { "type": "list", - "key": "defaults", + "key": "default_variants", "label": "Default Subsets", "object_type": "text" } @@ -177,7 +179,7 @@ }, { "type": "list", - "key": "defaults", + "key": "default_variants", "label": "Default Subsets", "object_type": "text" } @@ -212,7 +214,7 @@ }, { "type": "list", - "key": "defaults", + "key": "default_variants", "label": "Default Subsets", "object_type": "text" } @@ -242,7 +244,7 @@ }, { "type": "list", - "key": "defaults", + "key": "default_variants", "label": "Default Subsets", "object_type": "text" } @@ -262,7 +264,7 @@ }, { "type": "list", - "key": "defaults", + "key": "default_variants", "label": "Default Subsets", "object_type": "text" }, @@ -287,7 +289,7 @@ }, { "type": "list", - "key": "defaults", + "key": "default_variants", "label": "Default Subsets", "object_type": "text" }, @@ -389,7 +391,7 @@ }, { "type": "list", - "key": "defaults", + "key": "default_variants", "label": "Default Subsets", "object_type": "text" } diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create_render.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create_render.json index 68ad7ad63d..9d7432fe51 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create_render.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create_render.json @@ -12,9 +12,9 @@ }, { "type": "list", - "key": "defaults", + "key": "default_variants", "label": "Default Subsets", "object_type": "text" } ] -} \ No newline at end of file +} diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/template_create_plugin.json b/openpype/settings/entities/schemas/projects_schema/schemas/template_create_plugin.json index 14d15e7840..3d2ed9f3d4 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/template_create_plugin.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/template_create_plugin.json @@ -13,8 +13,8 @@ }, { "type": "list", - "key": "defaults", - "label": "Default Subsets", + "key": "default_variants", + "label": "Default Variants", "object_type": "text" } ] From 1b794361034c023acef1c3362df6065f1839245a Mon Sep 17 00:00:00 2001 From: Fabia Serra Arrizabalaga Date: Mon, 7 Aug 2023 19:08:50 +0200 Subject: [PATCH 041/327] Add function so Houdini creator settings get applied to instances --- openpype/hosts/houdini/api/plugin.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/openpype/hosts/houdini/api/plugin.py b/openpype/hosts/houdini/api/plugin.py index 1e7eaa7e22..a5efb73c67 100644 --- a/openpype/hosts/houdini/api/plugin.py +++ b/openpype/hosts/houdini/api/plugin.py @@ -292,3 +292,22 @@ class HoudiniCreator(NewCreator, HoudiniCreatorBase): """ return [hou.ropNodeTypeCategory()] + + def get_creator_settings(self, project_settings, settings_key=None): + if not settings_key: + settings_key = self.__class__.__name__ + return project_settings["houdini"]["create"][settings_key] + + def apply_settings( + self, + project_settings, + system_settings + ): + """Method called on initialization of plugin to apply settings.""" + + # plugin settings + plugin_settings = self.get_creator_settings(project_settings) + + # individual attributes + self.default_variants = plugin_settings.get( + "default_variants") or self.default_variants From 73bbc86d8b54dbe7859737f34d6db71120d6f693 Mon Sep 17 00:00:00 2001 From: Fabia Serra Arrizabalaga Date: Mon, 7 Aug 2023 19:09:27 +0200 Subject: [PATCH 042/327] Use default variant from creator plugin for interactive shelve tool --- .../hosts/houdini/api/creator_node_shelves.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/houdini/api/creator_node_shelves.py b/openpype/hosts/houdini/api/creator_node_shelves.py index 7c6122cffe..c724acb16d 100644 --- a/openpype/hosts/houdini/api/creator_node_shelves.py +++ b/openpype/hosts/houdini/api/creator_node_shelves.py @@ -35,11 +35,11 @@ CATEGORY_GENERIC_TOOL = { CREATE_SCRIPT = """ from openpype.hosts.houdini.api.creator_node_shelves import create_interactive -create_interactive("{identifier}", **kwargs) +create_interactive("{identifier}", "{variant}", **kwargs) """ -def create_interactive(creator_identifier, **kwargs): +def create_interactive(creator_identifier, default_variant, **kwargs): """Create a Creator using its identifier interactively. This is used by the generated shelf tools as callback when a user selects @@ -59,9 +59,9 @@ def create_interactive(creator_identifier, **kwargs): """ # TODO Use Qt instead - result, variant = hou.ui.readInput('Define variant name', + result, variant = hou.ui.readInput("Define variant name", buttons=("Ok", "Cancel"), - initial_contents='Main', + initial_contents=default_variant, title="Define variant", help="Set the variant for the " "publish instance", @@ -196,7 +196,14 @@ def install(): key = "openpype_create.{}".format(identifier) log.debug(f"Registering {key}") - script = CREATE_SCRIPT.format(identifier=identifier) + default_variant = "Main" + if hasattr(creator, "default_variant"): + default_variant = creator.default_variant + elif hasattr(creator, "default_variants"): + default_variant = creator.default_variants[0] + script = CREATE_SCRIPT.format( + identifier=identifier, variant=default_variant + ) data = { "script": script, "language": hou.scriptLanguage.Python, From 3f626080d6fd042e5f5a5505d8ff08ae298b8b45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A0=20Serra=20Arrizabalaga?= Date: Mon, 7 Aug 2023 19:28:19 +0200 Subject: [PATCH 043/327] Remove default_variants class variable --- openpype/hosts/houdini/plugins/create/create_arnold_ass.py | 2 -- openpype/hosts/houdini/plugins/create/create_arnold_rop.py | 1 - openpype/hosts/houdini/plugins/create/create_karma_rop.py | 1 - openpype/hosts/houdini/plugins/create/create_mantra_rop.py | 1 - openpype/hosts/houdini/plugins/create/create_redshift_rop.py | 1 - openpype/hosts/houdini/plugins/create/create_vray_rop.py | 1 - 6 files changed, 7 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_arnold_ass.py b/openpype/hosts/houdini/plugins/create/create_arnold_ass.py index 72b12ddba2..12d08f7d83 100644 --- a/openpype/hosts/houdini/plugins/create/create_arnold_ass.py +++ b/openpype/hosts/houdini/plugins/create/create_arnold_ass.py @@ -10,8 +10,6 @@ class CreateArnoldAss(plugin.HoudiniCreator): label = "Arnold ASS" family = "ass" icon = "magic" - default_variants = ["Main"] - # Default extension: `.ass` or `.ass.gz` # however calling HoudiniCreator.create() diff --git a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py index 7d00e8b3e5..b58c377a20 100644 --- a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py @@ -9,7 +9,6 @@ class CreateArnoldRop(plugin.HoudiniCreator): label = "Arnold ROP" family = "arnold_rop" icon = "magic" - default_variants = ["master"] # Default extension ext = "exr" diff --git a/openpype/hosts/houdini/plugins/create/create_karma_rop.py b/openpype/hosts/houdini/plugins/create/create_karma_rop.py index cb9c6dd711..4e1360ca45 100644 --- a/openpype/hosts/houdini/plugins/create/create_karma_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_karma_rop.py @@ -11,7 +11,6 @@ class CreateKarmaROP(plugin.HoudiniCreator): label = "Karma ROP" family = "karma_rop" icon = "magic" - default_variants = ["master"] def create(self, subset_name, instance_data, pre_create_data): import hou # noqa diff --git a/openpype/hosts/houdini/plugins/create/create_mantra_rop.py b/openpype/hosts/houdini/plugins/create/create_mantra_rop.py index b2846d53fa..d2f0e735a8 100644 --- a/openpype/hosts/houdini/plugins/create/create_mantra_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_mantra_rop.py @@ -11,7 +11,6 @@ class CreateMantraROP(plugin.HoudiniCreator): label = "Mantra ROP" family = "mantra_rop" icon = "magic" - default_variants = ["master"] def create(self, subset_name, instance_data, pre_create_data): import hou # noqa diff --git a/openpype/hosts/houdini/plugins/create/create_redshift_rop.py b/openpype/hosts/houdini/plugins/create/create_redshift_rop.py index 9bb1d58d8f..e37718129c 100644 --- a/openpype/hosts/houdini/plugins/create/create_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_redshift_rop.py @@ -13,7 +13,6 @@ class CreateRedshiftROP(plugin.HoudiniCreator): label = "Redshift ROP" family = "redshift_rop" icon = "magic" - default_variants = ["master"] ext = "exr" diff --git a/openpype/hosts/houdini/plugins/create/create_vray_rop.py b/openpype/hosts/houdini/plugins/create/create_vray_rop.py index a3ae01f5ae..34c8906bb0 100644 --- a/openpype/hosts/houdini/plugins/create/create_vray_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_vray_rop.py @@ -14,7 +14,6 @@ class CreateVrayROP(plugin.HoudiniCreator): label = "VRay ROP" family = "vray_rop" icon = "magic" - default_variants = ["master"] ext = "exr" From adca11d44bed5c705d4c3e7d9740f732053b7779 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A0=20Serra=20Arrizabalaga?= Date: Mon, 7 Aug 2023 19:28:58 +0200 Subject: [PATCH 044/327] Remove extra whitespace --- openpype/hosts/houdini/plugins/create/create_redshift_rop.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/create/create_redshift_rop.py b/openpype/hosts/houdini/plugins/create/create_redshift_rop.py index e37718129c..1b8826a932 100644 --- a/openpype/hosts/houdini/plugins/create/create_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_redshift_rop.py @@ -13,7 +13,6 @@ class CreateRedshiftROP(plugin.HoudiniCreator): label = "Redshift ROP" family = "redshift_rop" icon = "magic" - ext = "exr" def create(self, subset_name, instance_data, pre_create_data): From 84e3ade492bf51084824d520fb914a334a2361d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A0=20Serra=20Arrizabalaga?= Date: Mon, 7 Aug 2023 19:30:16 +0200 Subject: [PATCH 045/327] Revert merge conflict --- .../schemas/projects_schema/schemas/schema_maya_create.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json index 8faf3fcae8..10d5bff445 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json @@ -12,9 +12,8 @@ "checkbox_key": "enabled", "children": [ { - "type": "boolean", - "key": "enabled", - "label": "Enabled" + "type": "label", + "label": "This plugin is not optional due to implicit creation through loading the \"rig\" family.\nThis family is also hidden from creation due to complexity in setup." }, { "type": "boolean", From b57c8a79c090332ce488dc6daad78c77f7d59240 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A0=20Serra=20Arrizabalaga?= Date: Mon, 7 Aug 2023 19:33:46 +0200 Subject: [PATCH 046/327] Remove extra whitespace --- openpype/hosts/houdini/plugins/create/create_vray_rop.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/create/create_vray_rop.py b/openpype/hosts/houdini/plugins/create/create_vray_rop.py index 34c8906bb0..793a544fdf 100644 --- a/openpype/hosts/houdini/plugins/create/create_vray_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_vray_rop.py @@ -14,7 +14,6 @@ class CreateVrayROP(plugin.HoudiniCreator): label = "VRay ROP" family = "vray_rop" icon = "magic" - ext = "exr" def create(self, subset_name, instance_data, pre_create_data): From bb42369af27992a125ba7110954cd5a365cad542 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A0=20Serra=20Arrizabalaga?= Date: Mon, 7 Aug 2023 19:34:51 +0200 Subject: [PATCH 047/327] Fix merge conflict --- .../projects_schema/schemas/schema_maya_create.json | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json index 10d5bff445..8dec0a8817 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json @@ -12,8 +12,9 @@ "checkbox_key": "enabled", "children": [ { - "type": "label", - "label": "This plugin is not optional due to implicit creation through loading the \"rig\" family.\nThis family is also hidden from creation due to complexity in setup." + "type": "boolean", + "key": "enabled", + "label": "Enabled" }, { "type": "boolean", @@ -119,12 +120,10 @@ "collapsible": true, "key": "CreateAnimation", "label": "Create Animation", - "checkbox_key": "enabled", "children": [ { - "type": "boolean", - "key": "enabled", - "label": "Enabled" + "type": "label", + "label": "This plugin is not optional due to implicit creation through loading the \"rig\" family.\nThis family is also hidden from creation due to complexity in setup." }, { "type": "boolean", From 92254e2abc32d2305497dadf9319ebdef040ee14 Mon Sep 17 00:00:00 2001 From: Fabia Serra Arrizabalaga Date: Mon, 7 Aug 2023 19:53:32 +0200 Subject: [PATCH 048/327] Add a default variant to all Houdini creators --- openpype/hosts/houdini/api/plugin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/houdini/api/plugin.py b/openpype/hosts/houdini/api/plugin.py index 70c837205e..3d3b0e49b9 100644 --- a/openpype/hosts/houdini/api/plugin.py +++ b/openpype/hosts/houdini/api/plugin.py @@ -169,6 +169,8 @@ class HoudiniCreator(NewCreator, HoudiniCreatorBase): selected_nodes = [] settings_name = None + default_variant = "Main" + def create(self, subset_name, instance_data, pre_create_data): try: self.selected_nodes = [] From 9604656017cf06fb444435dcd37c15e8681719ee Mon Sep 17 00:00:00 2001 From: Fabia Serra Arrizabalaga Date: Mon, 7 Aug 2023 20:21:13 +0200 Subject: [PATCH 049/327] Add default variant on all creators --- .../defaults/project_settings/houdini.json | 29 +++++++++++++++---- .../defaults/project_settings/maya.json | 23 +++++++++++++++ .../schemas/schema_houdini_create.json | 5 ++++ .../schemas/schema_maya_create.json | 5 ++++ .../schemas/template_create_plugin.json | 5 ++++ 5 files changed, 62 insertions(+), 5 deletions(-) diff --git a/openpype/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json index 389fdc6d96..cf5d1c93d5 100644 --- a/openpype/settings/defaults/project_settings/houdini.json +++ b/openpype/settings/defaults/project_settings/houdini.json @@ -14,80 +14,99 @@ "create": { "CreateArnoldAss": { "enabled": true, + "default_variant": "Main", "default_variants": [], "ext": ".ass" }, "CreateArnoldRop": { "enabled": true, - "default_variants": [] + "default_variant": "master", + "default_variants": ["master"] }, "CreateAlembicCamera": { "enabled": true, + "default_variant": "Main", "default_variants": [] }, "CreateBGEO": { "enabled": true, + "default_variant": "Main", "default_variants": [] }, "CreateCompositeSequence": { "enabled": true, + "default_variant": "Main", "default_variants": [] }, "CreateHDA": { "enabled": true, + "default_variant": "Main", "default_variants": [] }, "CreateKarmaROP": { "enabled": true, - "default_variants": [] + "default_variant": "master", + "default_variants": ["master"] }, "CreateMantraROP": { "enabled": true, - "default_variants": [] + "default_variant": "master", + "default_variants": ["master"] }, "CreatePointCache": { "enabled": true, + "default_variant": "Main", "default_variants": [] }, "CreateRedshiftProxy": { "enabled": true, - "default_variants": [] + "default_variant": "master", + "default_variants": ["master"] }, "CreateRedshiftROP": { "enabled": true, + "default_variant": "Main", "default_variants": [] }, "CreateRemotePublish": { "enabled": true, + "default_variant": "Main", "default_variants": [] }, "CreateReview": { "enabled": true, + "default_variant": "Main", "default_variants": [] }, "CreateUSD": { "enabled": false, + "default_variant": "Main", "default_variants": [] }, "CreateUSDModel": { "enabled": false, + "default_variant": "Main", "default_variants": [] }, "USDCreateShadingWorkspace": { "enabled": false, + "default_variant": "Main", "default_variants": [] }, "CreateUSDRender": { "enabled": false, + "default_variant": "Main", "default_variants": [] }, "CreateVDBCache": { "enabled": true, + "default_variant": "Main", "default_variants": [] }, "CreateVrayROP": { "enabled": true, - "default_variants": [] + "default_variant": "master", + "default_variants": ["master"] } }, "publish": { diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index b9817fe400..f53501c7ac 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -521,18 +521,21 @@ "enabled": true, "make_tx": true, "rs_tex": false, + "default_variant": "Main", "default_variants": [ "Main" ] }, "CreateRender": { "enabled": true, + "default_variant": "Main", "default_variants": [ "Main" ] }, "CreateUnrealStaticMesh": { "enabled": true, + "default_variant": "", "default_variants": [ "", "_Main" @@ -547,6 +550,7 @@ }, "CreateUnrealSkeletalMesh": { "enabled": true, + "default_variant": "", "default_variants": [], "joint_hints": "jnt_org" }, @@ -560,6 +564,7 @@ "write_face_sets": false, "include_parent_hierarchy": false, "include_user_defined_attributes": false, + "default_variant": "Main", "default_variants": [ "Main" ] @@ -568,6 +573,7 @@ "enabled": true, "write_color_sets": false, "write_face_sets": false, + "default_variant": "Main", "default_variants": [ "Main", "Proxy", @@ -579,6 +585,7 @@ "write_color_sets": false, "write_face_sets": false, "include_user_defined_attributes": false, + "default_variant": "Main", "default_variants": [ "Main" ] @@ -587,12 +594,14 @@ "enabled": true, "write_color_sets": false, "write_face_sets": false, + "default_variant": "Main", "default_variants": [ "Main" ] }, "CreateReview": { "enabled": true, + "default_variant": "Main", "default_variants": [ "Main" ], @@ -600,6 +609,7 @@ }, "CreateAss": { "enabled": true, + "default_variant": "Main", "default_variants": [ "Main" ], @@ -622,60 +632,70 @@ "enabled": true, "vrmesh": true, "alembic": true, + "default_variant": "Main", "default_variants": [ "Main" ] }, "CreateMultiverseUsd": { "enabled": true, + "default_variant": "Main", "default_variants": [ "Main" ] }, "CreateMultiverseUsdComp": { "enabled": true, + "default_variant": "Main", "default_variants": [ "Main" ] }, "CreateMultiverseUsdOver": { "enabled": true, + "default_variant": "Main", "default_variants": [ "Main" ] }, "CreateAssembly": { "enabled": true, + "default_variant": "Main", "default_variants": [ "Main" ] }, "CreateCamera": { "enabled": true, + "default_variant": "Main", "default_variants": [ "Main" ] }, "CreateLayout": { "enabled": true, + "default_variant": "Main", "default_variants": [ "Main" ] }, "CreateMayaScene": { "enabled": true, + "default_variant": "Main", "default_variants": [ "Main" ] }, "CreateRenderSetup": { "enabled": true, + "default_variant": "Main", "default_variants": [ "Main" ] }, "CreateRig": { "enabled": true, + "default_variant": "Main", "default_variants": [ "Main", "Sim", @@ -684,6 +704,7 @@ }, "CreateSetDress": { "enabled": true, + "default_variant": "Main", "default_variants": [ "Main", "Anim" @@ -691,12 +712,14 @@ }, "CreateVRayScene": { "enabled": true, + "default_variant": "Main", "default_variants": [ "Main" ] }, "CreateYetiRig": { "enabled": true, + "default_variant": "Main", "default_variants": [ "Main" ] diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_create.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_create.json index a1736c811d..6e1eaf7146 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_create.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_create.json @@ -16,6 +16,11 @@ "key": "enabled", "label": "Enabled" }, + { + "type": "text", + "key": "default_variant", + "label": "Default variant" + }, { "type": "list", "key": "default_variants", diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json index 8dec0a8817..a8105bdb5d 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json @@ -387,6 +387,11 @@ "key": "alembic", "label": "Alembic" }, + { + "type": "text", + "key": "default_variant", + "label": "Default variant" + }, { "type": "list", "key": "default_variants", diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/template_create_plugin.json b/openpype/settings/entities/schemas/projects_schema/schemas/template_create_plugin.json index 3d2ed9f3d4..7384060625 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/template_create_plugin.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/template_create_plugin.json @@ -11,6 +11,11 @@ "key": "enabled", "label": "Enabled" }, + { + "type": "text", + "key": "default_variant", + "label": "Default variant" + }, { "type": "list", "key": "default_variants", From 8a02654dbc133563fe57a0f7190fd7f72b2210ec Mon Sep 17 00:00:00 2001 From: Fabia Serra Arrizabalaga Date: Mon, 7 Aug 2023 20:24:31 +0200 Subject: [PATCH 050/327] Fill up default variants --- .../defaults/project_settings/houdini.json | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/openpype/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json index cf5d1c93d5..630b189743 100644 --- a/openpype/settings/defaults/project_settings/houdini.json +++ b/openpype/settings/defaults/project_settings/houdini.json @@ -15,7 +15,7 @@ "CreateArnoldAss": { "enabled": true, "default_variant": "Main", - "default_variants": [], + "default_variants": ["Main"], "ext": ".ass" }, "CreateArnoldRop": { @@ -26,22 +26,22 @@ "CreateAlembicCamera": { "enabled": true, "default_variant": "Main", - "default_variants": [] + "default_variants": ["Main"] }, "CreateBGEO": { "enabled": true, "default_variant": "Main", - "default_variants": [] + "default_variants": ["Main"] }, "CreateCompositeSequence": { "enabled": true, "default_variant": "Main", - "default_variants": [] + "default_variants": ["Main"] }, "CreateHDA": { "enabled": true, "default_variant": "Main", - "default_variants": [] + "default_variants": ["Main"] }, "CreateKarmaROP": { "enabled": true, @@ -56,7 +56,7 @@ "CreatePointCache": { "enabled": true, "default_variant": "Main", - "default_variants": [] + "default_variants": ["Main"] }, "CreateRedshiftProxy": { "enabled": true, @@ -66,42 +66,42 @@ "CreateRedshiftROP": { "enabled": true, "default_variant": "Main", - "default_variants": [] + "default_variants": ["Main"] }, "CreateRemotePublish": { "enabled": true, "default_variant": "Main", - "default_variants": [] + "default_variants": ["Main"] }, "CreateReview": { "enabled": true, "default_variant": "Main", - "default_variants": [] + "default_variants": ["Main"] }, "CreateUSD": { "enabled": false, "default_variant": "Main", - "default_variants": [] + "default_variants": ["Main"] }, "CreateUSDModel": { "enabled": false, "default_variant": "Main", - "default_variants": [] + "default_variants": ["Main"] }, "USDCreateShadingWorkspace": { "enabled": false, "default_variant": "Main", - "default_variants": [] + "default_variants": ["Main"] }, "CreateUSDRender": { "enabled": false, "default_variant": "Main", - "default_variants": [] + "default_variants": ["Main"] }, "CreateVDBCache": { "enabled": true, "default_variant": "Main", - "default_variants": [] + "default_variants": ["Main"] }, "CreateVrayROP": { "enabled": true, From 26851293b4bb26805e2070aacb37741f74f32283 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 7 Aug 2023 21:48:52 +0200 Subject: [PATCH 051/327] simplification of code --- .../nuke/plugins/publish/collect_writes.py | 48 ++++++++++++++----- 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/collect_writes.py b/openpype/hosts/nuke/plugins/publish/collect_writes.py index fce2e571a0..d3899e0035 100644 --- a/openpype/hosts/nuke/plugins/publish/collect_writes.py +++ b/openpype/hosts/nuke/plugins/publish/collect_writes.py @@ -269,15 +269,14 @@ class CollectNukeWrites(pyblish.api.InstancePlugin, "tags": [] } - frame_start_str = "%0{}d".format( - len(str(last_frame))) % first_frame + frame_start_str = self._get_frame_start_str(first_frame, last_frame) + representation['frameStart'] = frame_start_str # set slate frame collected_frames = self._add_slate_frame_to_collected_frames( instance, collected_frames, - frame_start_str, first_frame, last_frame ) @@ -289,15 +288,40 @@ class CollectNukeWrites(pyblish.api.InstancePlugin, return representation + def _get_frame_start_str(self, first_frame, last_frame): + """Get frame start string. + + Args: + first_frame (int): first frame + last_frame (int): last frame + + Returns: + str: frame start string + """ + # convert first frame to string with padding + return ( + "{{:0{}d}}".format(len(str(last_frame))) + ).format(first_frame) + def _add_slate_frame_to_collected_frames( self, instance, collected_frames, - frame_start_str, first_frame, last_frame ): - """Set slate frame.""" + """Add slate frame to collected frames. + + Args: + instance (pyblish.api.Instance): pyblish instance + collected_frames (list): collected frames + first_frame (int): first frame + last_frame (int): last frame + + Returns: + list: collected frames + """ + frame_start_str = self._get_frame_start_str(first_frame, last_frame) frame_length = int(last_frame - first_frame + 1) # this will only run if slate frame is not already @@ -305,15 +329,15 @@ class CollectNukeWrites(pyblish.api.InstancePlugin, if ( "slate" in instance.data["families"] and frame_length == len(collected_frames) - and instance.data["family"] == "render" ): - frame_slate_str = ( - "{{:0{}d}}".format(len(str(last_frame))) - ).format(first_frame - 1) + frame_slate_str = self._get_frame_start_str( + first_frame - 1, + last_frame + ) - slate_frame = collected_frames[0].replace( - frame_start_str, frame_slate_str) - collected_frames.insert(0, slate_frame) + slate_frame = collected_frames[0].replace( + frame_start_str, frame_slate_str) + collected_frames.insert(0, slate_frame) return collected_frames From 97664e1bbd9c71b17deb7dd4e7c40bbc8b2a5648 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Mon, 7 Aug 2023 21:50:23 +0200 Subject: [PATCH 052/327] Update openpype/hosts/nuke/plugins/publish/collect_writes.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fabià Serra Arrizabalaga --- openpype/hosts/nuke/plugins/publish/collect_writes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/collect_writes.py b/openpype/hosts/nuke/plugins/publish/collect_writes.py index d3899e0035..bf9740563c 100644 --- a/openpype/hosts/nuke/plugins/publish/collect_writes.py +++ b/openpype/hosts/nuke/plugins/publish/collect_writes.py @@ -144,7 +144,7 @@ class CollectNukeWrites(pyblish.api.InstancePlugin, instance.data["families"].append( "{}.{}".format(family, render_target) ) - self.log.info("Appending render target to families: {}.{}".format( + self.log.debug("Appending render target to families: {}.{}".format( family, render_target) ) From f4284e5e646d74d29fea23c25b1d86774f4de909 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Mon, 7 Aug 2023 21:51:10 +0200 Subject: [PATCH 053/327] Update openpype/hosts/nuke/plugins/publish/collect_writes.py Co-authored-by: Roy Nieterau --- openpype/hosts/nuke/plugins/publish/collect_writes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/collect_writes.py b/openpype/hosts/nuke/plugins/publish/collect_writes.py index bf9740563c..f1b36ba886 100644 --- a/openpype/hosts/nuke/plugins/publish/collect_writes.py +++ b/openpype/hosts/nuke/plugins/publish/collect_writes.py @@ -386,10 +386,10 @@ class CollectNukeWrites(pyblish.api.InstancePlugin, })) # convert only to base names - expected_filenames = [ + expected_filenames = { os.path.basename(filepath) for filepath in expected_paths - ] + } # make sure files are existing at folder collected_frames = [ From 7d40debd64e3e4c097a49d3c0714c3d532c8d5ae Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 8 Aug 2023 11:00:02 +0200 Subject: [PATCH 054/327] Maya: do not create top level group on reference (#5402) * OP-6358 - allow not creation of group for reference loader in Maya Creation of wrapping group could be controlled by checkbox in ReferenceLoader options. * OP-6358 - group name could be empty in Settings This enables default behavior of not creating wrapping group without need of artists unchecking `Group imported assets`. * OP-6358 - changed to safer logic Stripping of | was weird and potentially dangerous (collision of names), this logic should be safer. --- openpype/hosts/maya/api/plugin.py | 4 ++-- .../hosts/maya/plugins/load/load_reference.py | 15 ++++++++++++--- website/docs/admin_hosts_maya.md | 3 ++- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index bfe01c8981..4d467840dd 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -575,8 +575,8 @@ class ReferenceLoader(Loader): raise LoadError("No namespace specified in " "Maya ReferenceLoader settings") elif not custom_naming['group_name']: - raise LoadError("No group name specified in " - "Maya ReferenceLoader settings") + self.log.debug("No custom group_name, no group will be created.") + options["attach_to_root"] = False formatting_data = { "asset_name": asset['name'], diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index d339aff69c..c8d3b3128a 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -9,7 +9,8 @@ from openpype.hosts.maya.api.lib import ( maintained_selection, get_container_members, parent_nodes, - create_rig_animation_instance + create_rig_animation_instance, + get_reference_node ) @@ -123,6 +124,10 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): attach_to_root = options.get("attach_to_root", True) group_name = options["group_name"] + # no group shall be created + if not attach_to_root: + group_name = namespace + path = self.filepath_from_context(context) with maintained_selection(): cmds.loadPlugin("AbcImport.mll", quiet=True) @@ -148,11 +153,10 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): if current_namespace != ":": group_name = current_namespace + ":" + group_name - group_name = "|" + group_name - self[:] = new_nodes if attach_to_root: + group_name = "|" + group_name roots = cmds.listRelatives(group_name, children=True, fullPath=True) or [] @@ -205,6 +209,11 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): self._post_process_rig(name, namespace, context, options) else: if "translate" in options: + if not attach_to_root and new_nodes: + root_nodes = cmds.ls(new_nodes, assemblies=True, + long=True) + # we assume only a single root is ever loaded + group_name = root_nodes[0] cmds.setAttr("{}.translate".format(group_name), *options["translate"]) return new_nodes diff --git a/website/docs/admin_hosts_maya.md b/website/docs/admin_hosts_maya.md index 700822843f..93acf316c2 100644 --- a/website/docs/admin_hosts_maya.md +++ b/website/docs/admin_hosts_maya.md @@ -113,7 +113,8 @@ This is useful to fix some specific renderer glitches and advanced hacking of Ma #### Namespace and Group Name Here you can create your own custom naming for the reference loader. -The custom naming is split into two parts: namespace and group name. If you don't set the namespace or the group name, an error will occur. +The custom naming is split into two parts: namespace and group name. If you don't set the namespace, an error will occur. +Group name could be set empty, that way no wrapping group will be created for loaded item. Here's the different variables you can use:
From 7debe12c2bd4139c1e45b43bca04f7f8ec9fa6f9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 8 Aug 2023 11:36:16 +0200 Subject: [PATCH 055/327] Webpublisher: Cleanup targets (#5418) * remove remotepublish commands from cli * use 'automated' target instead ot 'remotepublish' * removed references to remotepublish * added webpublish target * use webpubish targets where automated does not make sense --- openpype/cli.py | 41 ----------- .../aftereffects/plugins/publish/closeAE.py | 2 +- .../photoshop/plugins/publish/closePS.py | 2 +- .../plugins/publish/collect_auto_image.py | 4 +- .../plugins/publish/collect_auto_review.py | 2 +- .../plugins/publish/collect_auto_workfile.py | 2 +- .../plugins/publish/collect_batch_data.py | 2 +- .../publish/collect_color_coded_instances.py | 2 +- .../publish/collect_published_version.py | 2 +- openpype/hosts/webpublisher/README.md | 2 +- openpype/hosts/webpublisher/lib.py | 2 +- .../hosts/webpublisher/publish_functions.py | 12 ++-- .../webserver_service/webpublish_routes.py | 6 +- .../webserver_service/webserver.py | 2 +- .../plugins/publish/collect_username.py | 2 +- openpype/pype_commands.py | 68 ------------------- 16 files changed, 21 insertions(+), 132 deletions(-) diff --git a/openpype/cli.py b/openpype/cli.py index 22ad16e937..0df277fb0a 100644 --- a/openpype/cli.py +++ b/openpype/cli.py @@ -196,47 +196,6 @@ def publish(paths, targets, gui): PypeCommands.publish(list(paths), targets, gui) -@main.command() -@click.argument("path") -@click.option("-h", "--host", help="Host") -@click.option("-u", "--user", help="User email address") -@click.option("-p", "--project", help="Project") -@click.option("-t", "--targets", help="Targets", default=None, - multiple=True) -def remotepublishfromapp(project, path, host, user=None, targets=None): - """Start CLI publishing. - - Publish collects json from paths provided as an argument. - More than one path is allowed. - """ - - if AYON_SERVER_ENABLED: - raise RuntimeError( - "AYON does not support 'remotepublishfromapp' command." - ) - PypeCommands.remotepublishfromapp( - project, path, host, user, targets=targets - ) - - -@main.command() -@click.argument("path") -@click.option("-u", "--user", help="User email address") -@click.option("-p", "--project", help="Project") -@click.option("-t", "--targets", help="Targets", default=None, - multiple=True) -def remotepublish(project, path, user=None, targets=None): - """Start CLI publishing. - - Publish collects json from paths provided as an argument. - More than one path is allowed. - """ - - if AYON_SERVER_ENABLED: - raise RuntimeError("AYON does not support 'remotepublish' command.") - PypeCommands.remotepublish(project, path, user, targets=targets) - - @main.command(context_settings={"ignore_unknown_options": True}) def projectmanager(): if AYON_SERVER_ENABLED: diff --git a/openpype/hosts/aftereffects/plugins/publish/closeAE.py b/openpype/hosts/aftereffects/plugins/publish/closeAE.py index eff2573e8f..0be20d9f05 100644 --- a/openpype/hosts/aftereffects/plugins/publish/closeAE.py +++ b/openpype/hosts/aftereffects/plugins/publish/closeAE.py @@ -15,7 +15,7 @@ class CloseAE(pyblish.api.ContextPlugin): active = True hosts = ["aftereffects"] - targets = ["remotepublish"] + targets = ["automated"] def process(self, context): self.log.info("CloseAE") diff --git a/openpype/hosts/photoshop/plugins/publish/closePS.py b/openpype/hosts/photoshop/plugins/publish/closePS.py index b4ded96001..b4c3a4c966 100644 --- a/openpype/hosts/photoshop/plugins/publish/closePS.py +++ b/openpype/hosts/photoshop/plugins/publish/closePS.py @@ -17,7 +17,7 @@ class ClosePS(pyblish.api.ContextPlugin): active = True hosts = ["photoshop"] - targets = ["remotepublish"] + targets = ["automated"] def process(self, context): self.log.info("ClosePS") diff --git a/openpype/hosts/photoshop/plugins/publish/collect_auto_image.py b/openpype/hosts/photoshop/plugins/publish/collect_auto_image.py index ce408f8d01..f1d8419608 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_auto_image.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_auto_image.py @@ -6,8 +6,6 @@ from openpype.pipeline.create import get_subset_name class CollectAutoImage(pyblish.api.ContextPlugin): """Creates auto image in non artist based publishes (Webpublisher). - - 'remotepublish' should be renamed to 'autopublish' or similar in the future """ label = "Collect Auto Image" @@ -15,7 +13,7 @@ class CollectAutoImage(pyblish.api.ContextPlugin): hosts = ["photoshop"] order = pyblish.api.CollectorOrder + 0.2 - targets = ["remotepublish"] + targets = ["automated"] def process(self, context): family = "image" diff --git a/openpype/hosts/photoshop/plugins/publish/collect_auto_review.py b/openpype/hosts/photoshop/plugins/publish/collect_auto_review.py index 7de4adcaf4..82ba0ac09c 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_auto_review.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_auto_review.py @@ -20,7 +20,7 @@ class CollectAutoReview(pyblish.api.ContextPlugin): label = "Collect Auto Review" hosts = ["photoshop"] order = pyblish.api.CollectorOrder + 0.2 - targets = ["remotepublish"] + targets = ["automated"] publish = True diff --git a/openpype/hosts/photoshop/plugins/publish/collect_auto_workfile.py b/openpype/hosts/photoshop/plugins/publish/collect_auto_workfile.py index d10cf62c67..01dc50af40 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_auto_workfile.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_auto_workfile.py @@ -12,7 +12,7 @@ class CollectAutoWorkfile(pyblish.api.ContextPlugin): label = "Collect Workfile" hosts = ["photoshop"] - targets = ["remotepublish"] + targets = ["automated"] def process(self, context): family = "workfile" diff --git a/openpype/hosts/photoshop/plugins/publish/collect_batch_data.py b/openpype/hosts/photoshop/plugins/publish/collect_batch_data.py index a5fea7ac7d..b13ff5e476 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_batch_data.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_batch_data.py @@ -35,7 +35,7 @@ class CollectBatchData(pyblish.api.ContextPlugin): order = pyblish.api.CollectorOrder - 0.495 label = "Collect batch data" hosts = ["photoshop"] - targets = ["remotepublish"] + targets = ["webpublish"] def process(self, context): self.log.info("CollectBatchData") diff --git a/openpype/hosts/photoshop/plugins/publish/collect_color_coded_instances.py b/openpype/hosts/photoshop/plugins/publish/collect_color_coded_instances.py index 90fca8398f..c16616bcb2 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_color_coded_instances.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_color_coded_instances.py @@ -34,7 +34,7 @@ class CollectColorCodedInstances(pyblish.api.ContextPlugin): label = "Instances" order = pyblish.api.CollectorOrder hosts = ["photoshop"] - targets = ["remotepublish"] + targets = ["automated"] # configurable by Settings color_code_mapping = [] diff --git a/openpype/hosts/photoshop/plugins/publish/collect_published_version.py b/openpype/hosts/photoshop/plugins/publish/collect_published_version.py index 2502689e4b..7371c0564f 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_published_version.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_published_version.py @@ -26,7 +26,7 @@ class CollectPublishedVersion(pyblish.api.ContextPlugin): order = pyblish.api.CollectorOrder + 0.190 label = "Collect published version" hosts = ["photoshop"] - targets = ["remotepublish"] + targets = ["automated"] def process(self, context): workfile_subset_name = None diff --git a/openpype/hosts/webpublisher/README.md b/openpype/hosts/webpublisher/README.md index 0826e44490..07a957fa7f 100644 --- a/openpype/hosts/webpublisher/README.md +++ b/openpype/hosts/webpublisher/README.md @@ -3,4 +3,4 @@ Webpublisher Plugins meant for processing of Webpublisher. -Gets triggered by calling openpype.cli.remotepublish with appropriate arguments. \ No newline at end of file +Gets triggered by calling `openpype_console modules webpublisher publish` with appropriate arguments. diff --git a/openpype/hosts/webpublisher/lib.py b/openpype/hosts/webpublisher/lib.py index 11c287761b..ecd28d2432 100644 --- a/openpype/hosts/webpublisher/lib.py +++ b/openpype/hosts/webpublisher/lib.py @@ -270,7 +270,7 @@ def find_variant_key(application_manager, host): def get_task_data(batch_dir): """Return parsed data from first task manifest.json - Used for `remotepublishfromapp` command where batch contains only + Used for `publishfromapp` command where batch contains only single task with publishable workfile. Returns: diff --git a/openpype/hosts/webpublisher/publish_functions.py b/openpype/hosts/webpublisher/publish_functions.py index 41aab68cce..f5dc88f54d 100644 --- a/openpype/hosts/webpublisher/publish_functions.py +++ b/openpype/hosts/webpublisher/publish_functions.py @@ -34,7 +34,7 @@ def cli_publish(project_name, batch_path, user_email, targets): Args: project_name (str): project to publish (only single context is - expected per call of remotepublish + expected per call of 'publish') batch_path (str): Path batch folder. Contains subfolders with resources (workfile, another subfolder 'renders' etc.) user_email (string): email address for webpublisher - used to @@ -49,8 +49,8 @@ def cli_publish(project_name, batch_path, user_email, targets): if not batch_path: raise RuntimeError("No publish paths specified") - log = Logger.get_logger("remotepublish") - log.info("remotepublish command") + log = Logger.get_logger("Webpublish") + log.info("Webpublish command") # Register target and host webpublisher_host = WebpublisherHost() @@ -107,7 +107,7 @@ def cli_publish_from_app( Args: project_name (str): project to publish (only single context is - expected per call of remotepublish + expected per call of publish batch_path (str): Path batch folder. Contains subfolders with resources (workfile, another subfolder 'renders' etc.) host_name (str): 'photoshop' @@ -117,9 +117,9 @@ def cli_publish_from_app( (to choose validator for example) """ - log = Logger.get_logger("RemotePublishFromApp") + log = Logger.get_logger("PublishFromApp") - log.info("remotepublishphotoshop command") + log.info("Webpublish photoshop command") task_data = get_task_data(batch_path) diff --git a/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py b/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py index 9fe4b4d3c1..e56f245d27 100644 --- a/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py +++ b/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py @@ -216,7 +216,7 @@ class BatchPublishEndpoint(WebpublishApiEndpoint): "extensions": [".tvpp"], "command": "publish", "arguments": { - "targets": ["tvpaint_worker"] + "targets": ["tvpaint_worker", "webpublish"] }, "add_to_queue": False }, @@ -230,7 +230,7 @@ class BatchPublishEndpoint(WebpublishApiEndpoint): # Make sure targets are set to None for cases that default # would change # - targets argument is not used in 'publishfromapp' - "targets": ["remotepublish"] + "targets": ["automated", "webpublish"] }, # does publish need to be handled by a queue, eg. only # single process running concurrently? @@ -247,7 +247,7 @@ class BatchPublishEndpoint(WebpublishApiEndpoint): "project": content["project_name"], "user": content["user"], - "targets": ["filespublish"] + "targets": ["filespublish", "webpublish"] } add_to_queue = False diff --git a/openpype/hosts/webpublisher/webserver_service/webserver.py b/openpype/hosts/webpublisher/webserver_service/webserver.py index 093b53d9d3..d7c2ea01b9 100644 --- a/openpype/hosts/webpublisher/webserver_service/webserver.py +++ b/openpype/hosts/webpublisher/webserver_service/webserver.py @@ -45,7 +45,7 @@ def run_webserver(executable, upload_dir, host=None, port=None): server_manager = webserver_module.create_new_server_manager(port, host) webserver_url = server_manager.url - # queue for remotepublishfromapp tasks + # queue for publishfromapp tasks studio_task_queue = collections.deque() resource = RestApiResource(server_manager, diff --git a/openpype/modules/ftrack/plugins/publish/collect_username.py b/openpype/modules/ftrack/plugins/publish/collect_username.py index 798f3960a8..0c7c0a57be 100644 --- a/openpype/modules/ftrack/plugins/publish/collect_username.py +++ b/openpype/modules/ftrack/plugins/publish/collect_username.py @@ -33,7 +33,7 @@ class CollectUsernameForWebpublish(pyblish.api.ContextPlugin): order = pyblish.api.CollectorOrder + 0.0015 label = "Collect ftrack username" hosts = ["webpublisher", "photoshop"] - targets = ["remotepublish", "filespublish", "tvpaint_worker"] + targets = ["webpublish"] def process(self, context): self.log.info("{}".format(self.__class__.__name__)) diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index 57bbb0bbe3..7f1c3b01e2 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -165,74 +165,6 @@ class PypeCommands: log.info("Publish finished.") - @staticmethod - def remotepublishfromapp(project_name, batch_path, host_name, - user_email, targets=None): - """Opens installed variant of 'host' and run remote publish there. - - Eventually should be yanked out to Webpublisher cli. - - Currently implemented and tested for Photoshop where customer - wants to process uploaded .psd file and publish collected layers - from there. Triggered by Webpublisher. - - Checks if no other batches are running (status =='in_progress). If - so, it sleeps for SLEEP (this is separate process), - waits for WAIT_FOR seconds altogether. - - Requires installed host application on the machine. - - Runs publish process as user would, in automatic fashion. - - Args: - project_name (str): project to publish (only single context is - expected per call of remotepublish - batch_path (str): Path batch folder. Contains subfolders with - resources (workfile, another subfolder 'renders' etc.) - host_name (str): 'photoshop' - user_email (string): email address for webpublisher - used to - find Ftrack user with same email - targets (list): Pyblish targets - (to choose validator for example) - """ - - from openpype.hosts.webpublisher.publish_functions import ( - cli_publish_from_app - ) - - cli_publish_from_app( - project_name, batch_path, host_name, user_email, targets - ) - - @staticmethod - def remotepublish(project, batch_path, user_email, targets=None): - """Start headless publishing. - - Used to publish rendered assets, workfiles etc via Webpublisher. - Eventually should be yanked out to Webpublisher cli. - - Publish use json from passed paths argument. - - Args: - project (str): project to publish (only single context is expected - per call of remotepublish - batch_path (str): Path batch folder. Contains subfolders with - resources (workfile, another subfolder 'renders' etc.) - user_email (string): email address for webpublisher - used to - find Ftrack user with same email - targets (list): Pyblish targets - (to choose validator for example) - - Raises: - RuntimeError: When there is no path to process. - """ - - from openpype.hosts.webpublisher.publish_functions import ( - cli_publish - ) - - cli_publish(project, batch_path, user_email, targets) - @staticmethod def extractenvironments(output_json_path, project, asset, task, app, env_group): From 38b905c6f92b7ff4b3b9977c27b77153a35f9e8d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 8 Aug 2023 18:27:13 +0800 Subject: [PATCH 056/327] restore the load max scene for resolving the possible conflict --- .../hosts/max/plugins/load/load_max_scene.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_max_scene.py b/openpype/hosts/max/plugins/load/load_max_scene.py index 468461bc0e..76cd3bf367 100644 --- a/openpype/hosts/max/plugins/load/load_max_scene.py +++ b/openpype/hosts/max/plugins/load/load_max_scene.py @@ -1,8 +1,7 @@ import os from openpype.hosts.max.api import lib -from openpype.hosts.max.api.pipeline import containerise, load_OpenpypeData - +from openpype.hosts.max.api.pipeline import containerise from openpype.pipeline import get_representation_path, load @@ -28,7 +27,6 @@ class MaxSceneLoader(load.LoaderPlugin): rt.MergeMaxFile(path) max_objects = rt.getLastMergedNodes() max_container = rt.Container(name=f"{name}") - load_OpenpypeData(max_container, max_objects) for max_object in max_objects: max_object.Parent = max_container @@ -41,16 +39,16 @@ class MaxSceneLoader(load.LoaderPlugin): path = get_representation_path(representation) node_name = container["instance_node"] - rt.MergeMaxFile(path) + rt.MergeMaxFile(path, + rt.Name("noRedraw"), + rt.Name("deleteOldDups"), + rt.Name("useSceneMtlDups")) max_objects = rt.getLastMergedNodes() container_node = rt.GetNodeByName(node_name) - instance_name, _ = os.path.splitext(node_name) - instance_container = rt.GetNodeByName(instance_name) for max_object in max_objects: - max_object.Parent = instance_container - instance_container.Parent = container_node - load_OpenpypeData(container_node, max_objects) + max_object.Parent = container_node + lib.imprint(container["instance_node"], { "representation": str(representation["_id"]) }) From fe2e6276f340740d9386c51cdfb902ba1f4ed7ec Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 8 Aug 2023 15:44:20 +0200 Subject: [PATCH 057/327] nuke: implementation of delete_placeholder to creator and loader plugins --- .../nuke/api/workfile_template_builder.py | 33 +++++++++++-------- .../workfile/workfile_template_builder.py | 18 ++++++++-- 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/openpype/hosts/nuke/api/workfile_template_builder.py b/openpype/hosts/nuke/api/workfile_template_builder.py index a19cb9dfea..5edf53be3b 100644 --- a/openpype/hosts/nuke/api/workfile_template_builder.py +++ b/openpype/hosts/nuke/api/workfile_template_builder.py @@ -114,6 +114,11 @@ class NukePlaceholderPlugin(PlaceholderPlugin): placeholder_data[key] = value return placeholder_data + def delete_placeholder(self, placeholder, failed): + """Remove placeholder if building was successful""" + placeholder_node = nuke.toNode(placeholder.scene_identifier) + nuke.delete(placeholder_node) + class NukePlaceholderLoadPlugin(NukePlaceholderPlugin, PlaceholderLoadMixin): identifier = "nuke.load" @@ -276,13 +281,13 @@ class NukePlaceholderLoadPlugin(NukePlaceholderPlugin, PlaceholderLoadMixin): placeholder.data["nb_children"] += 1 reset_selection() - # remove placeholders marked as delete - if ( - placeholder.data.get("delete") - and not placeholder.data.get("keep_placeholder") - ): - self.log.debug("Deleting node: {}".format(placeholder_node.name())) - nuke.delete(placeholder_node) + # # remove placeholders marked as delete + # if ( + # placeholder.data.get("delete") + # and not placeholder.data.get("keep_placeholder") + # ): + # self.log.debug("Deleting node: {}".format(placeholder_node.name())) + # nuke.delete(placeholder_node) # go back to root group nuke.root().begin() @@ -690,13 +695,13 @@ class NukePlaceholderCreatePlugin( placeholder.data["nb_children"] += 1 reset_selection() - # remove placeholders marked as delete - if ( - placeholder.data.get("delete") - and not placeholder.data.get("keep_placeholder") - ): - self.log.debug("Deleting node: {}".format(placeholder_node.name())) - nuke.delete(placeholder_node) + # # remove placeholders marked as delete + # if ( + # placeholder.data.get("delete") + # and not placeholder.data.get("keep_placeholder") + # ): + # self.log.debug("Deleting node: {}".format(placeholder_node.name())) + # nuke.delete(placeholder_node) # go back to root group nuke.root().begin() diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index bdb13415bf..25513b4d3c 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -1588,7 +1588,7 @@ class PlaceholderLoadMixin(object): ) return if not placeholder.data.get("keep_placeholder", True): - self.delete_placeholder(placeholder) + self.delete_placeholder(placeholder, failed) def load_failed(self, placeholder, representation): if hasattr(placeholder, "load_failed"): @@ -1781,6 +1781,17 @@ class PlaceholderCreateMixin(object): self.post_placeholder_process(placeholder, failed) + if failed: + self.log.debug( + "Placeholder cleanup skipped due to failed placeholder " + "population." + ) + return + + if not placeholder.data.get("keep_placeholder", True): + self.delete_placeholder(placeholder, failed) + + def create_failed(self, placeholder, creator_data): if hasattr(placeholder, "create_failed"): placeholder.create_failed(creator_data) @@ -1800,9 +1811,12 @@ class PlaceholderCreateMixin(object): representation. failed (bool): Loading of representation failed. """ - pass + def delete_placeholder(self, placeholder, failed): + """Called when all item population is done.""" + self.log.debug("Clean up of placeholder is not implemented.") + def _before_instance_create(self, placeholder): """Can be overriden. Is called before instance is created.""" From 7861b028372720fb740cfd4ca73499f2d833edc0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 8 Aug 2023 15:46:43 +0200 Subject: [PATCH 058/327] fixing inconsistency with input arguments --- openpype/hosts/nuke/api/workfile_template_builder.py | 2 +- openpype/pipeline/workfile/workfile_template_builder.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/nuke/api/workfile_template_builder.py b/openpype/hosts/nuke/api/workfile_template_builder.py index 5edf53be3b..c33b4d5776 100644 --- a/openpype/hosts/nuke/api/workfile_template_builder.py +++ b/openpype/hosts/nuke/api/workfile_template_builder.py @@ -114,7 +114,7 @@ class NukePlaceholderPlugin(PlaceholderPlugin): placeholder_data[key] = value return placeholder_data - def delete_placeholder(self, placeholder, failed): + def delete_placeholder(self, placeholder): """Remove placeholder if building was successful""" placeholder_node = nuke.toNode(placeholder.scene_identifier) nuke.delete(placeholder_node) diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index 25513b4d3c..b218a34868 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -1588,7 +1588,7 @@ class PlaceholderLoadMixin(object): ) return if not placeholder.data.get("keep_placeholder", True): - self.delete_placeholder(placeholder, failed) + self.delete_placeholder(placeholder) def load_failed(self, placeholder, representation): if hasattr(placeholder, "load_failed"): @@ -1612,7 +1612,7 @@ class PlaceholderLoadMixin(object): pass - def delete_placeholder(self, placeholder, failed): + def delete_placeholder(self, placeholder): """Called when all item population is done.""" self.log.debug("Clean up of placeholder is not implemented.") @@ -1789,7 +1789,7 @@ class PlaceholderCreateMixin(object): return if not placeholder.data.get("keep_placeholder", True): - self.delete_placeholder(placeholder, failed) + self.delete_placeholder(placeholder) def create_failed(self, placeholder, creator_data): @@ -1813,7 +1813,7 @@ class PlaceholderCreateMixin(object): """ pass - def delete_placeholder(self, placeholder, failed): + def delete_placeholder(self, placeholder): """Called when all item population is done.""" self.log.debug("Clean up of placeholder is not implemented.") From fa66c9f1e019f55fc1a601763db7151b72206fbe Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 8 Aug 2023 16:20:15 +0200 Subject: [PATCH 059/327] removing residual mess --- .../hosts/nuke/api/workfile_template_builder.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/openpype/hosts/nuke/api/workfile_template_builder.py b/openpype/hosts/nuke/api/workfile_template_builder.py index c33b4d5776..9d7604c58d 100644 --- a/openpype/hosts/nuke/api/workfile_template_builder.py +++ b/openpype/hosts/nuke/api/workfile_template_builder.py @@ -281,14 +281,6 @@ class NukePlaceholderLoadPlugin(NukePlaceholderPlugin, PlaceholderLoadMixin): placeholder.data["nb_children"] += 1 reset_selection() - # # remove placeholders marked as delete - # if ( - # placeholder.data.get("delete") - # and not placeholder.data.get("keep_placeholder") - # ): - # self.log.debug("Deleting node: {}".format(placeholder_node.name())) - # nuke.delete(placeholder_node) - # go back to root group nuke.root().begin() @@ -695,14 +687,6 @@ class NukePlaceholderCreatePlugin( placeholder.data["nb_children"] += 1 reset_selection() - # # remove placeholders marked as delete - # if ( - # placeholder.data.get("delete") - # and not placeholder.data.get("keep_placeholder") - # ): - # self.log.debug("Deleting node: {}".format(placeholder_node.name())) - # nuke.delete(placeholder_node) - # go back to root group nuke.root().begin() From ecbf263feb87d16bc67de2c3a4decd5a49ae479d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Tue, 8 Aug 2023 17:04:03 +0200 Subject: [PATCH 060/327] Update openpype/hosts/nuke/api/lib.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/nuke/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 5ea6752579..e6ba96ae9f 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2212,7 +2212,7 @@ Reopening Nuke should synchronize these paths and resolve any discrepancies. # use regex to find env var in template with format {ENV_VAR} # this way we make sure only template used env vars are included - env_var_regex = r"\{([A-Z_]+)\}" + env_var_regex = r"\{([A-Z0-9_]+)\}" env_var = re.findall(env_var_regex, config_template) if env_var: included_vars.append(env_var[0]) From a82421f3cc6617e562b7cebb0692f89a38e33206 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Tue, 8 Aug 2023 17:06:14 +0200 Subject: [PATCH 061/327] Update openpype/hosts/nuke/api/lib.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/nuke/api/lib.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index e6ba96ae9f..ec1dd07ab4 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2240,10 +2240,9 @@ Reopening Nuke should synchronize these paths and resolve any discrepancies. # make sure we return original path if no env var is included new_path = config_path - for env_var, env_path in os.environ.items(): - # first check if variable is whitelisted - if env_var not in included_vars: - # included vars not found in env_var name + for env_var in included_vars: + env_path = os.getenv(env_var) + if not env_path: continue # it has to be directory current process can see From 20479595061f2e08c9ca3b3bb710cbe0f3147956 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A0=20Serra=20Arrizabalaga?= Date: Tue, 8 Aug 2023 20:06:56 +0200 Subject: [PATCH 062/327] Make use of `get_default_variant` function --- openpype/hosts/houdini/api/creator_node_shelves.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/openpype/hosts/houdini/api/creator_node_shelves.py b/openpype/hosts/houdini/api/creator_node_shelves.py index c724acb16d..01da2fc3e1 100644 --- a/openpype/hosts/houdini/api/creator_node_shelves.py +++ b/openpype/hosts/houdini/api/creator_node_shelves.py @@ -196,13 +196,8 @@ def install(): key = "openpype_create.{}".format(identifier) log.debug(f"Registering {key}") - default_variant = "Main" - if hasattr(creator, "default_variant"): - default_variant = creator.default_variant - elif hasattr(creator, "default_variants"): - default_variant = creator.default_variants[0] script = CREATE_SCRIPT.format( - identifier=identifier, variant=default_variant + identifier=identifier, variant=creator.get_default_variant() ) data = { "script": script, From 2f663bc011ef4bd934f9cc7d690dfd3fd4769eda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A0=20Serra=20Arrizabalaga?= Date: Tue, 8 Aug 2023 20:20:08 +0200 Subject: [PATCH 063/327] Remove 'default_variant' from schemas --- .../defaults/project_settings/houdini.json | 57 +++++++------------ .../defaults/project_settings/maya.json | 24 -------- .../schemas/schema_houdini_create.json | 5 -- .../schemas/template_create_plugin.json | 5 -- 4 files changed, 19 insertions(+), 72 deletions(-) diff --git a/openpype/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json index 630b189743..512690bfd7 100644 --- a/openpype/settings/defaults/project_settings/houdini.json +++ b/openpype/settings/defaults/project_settings/houdini.json @@ -14,99 +14,80 @@ "create": { "CreateArnoldAss": { "enabled": true, - "default_variant": "Main", - "default_variants": ["Main"], + "default_variants": ["main"], "ext": ".ass" }, "CreateArnoldRop": { "enabled": true, - "default_variant": "master", - "default_variants": ["master"] + "default_variants": ["main"] }, "CreateAlembicCamera": { "enabled": true, - "default_variant": "Main", - "default_variants": ["Main"] + "default_variants": ["main"] }, "CreateBGEO": { "enabled": true, - "default_variant": "Main", - "default_variants": ["Main"] + "default_variants": ["main"] }, "CreateCompositeSequence": { "enabled": true, - "default_variant": "Main", - "default_variants": ["Main"] + "default_variants": ["main"] }, "CreateHDA": { "enabled": true, - "default_variant": "Main", - "default_variants": ["Main"] + "default_variants": ["main"] }, "CreateKarmaROP": { "enabled": true, - "default_variant": "master", - "default_variants": ["master"] + "default_variants": ["main"] }, "CreateMantraROP": { "enabled": true, - "default_variant": "master", - "default_variants": ["master"] + "default_variants": ["main"] }, "CreatePointCache": { "enabled": true, - "default_variant": "Main", - "default_variants": ["Main"] + "default_variants": ["main"] }, "CreateRedshiftProxy": { "enabled": true, - "default_variant": "master", - "default_variants": ["master"] + "default_variants": ["main"] }, "CreateRedshiftROP": { "enabled": true, - "default_variant": "Main", - "default_variants": ["Main"] + "default_variants": ["main"] }, "CreateRemotePublish": { "enabled": true, - "default_variant": "Main", - "default_variants": ["Main"] + "default_variants": ["main"] }, "CreateReview": { "enabled": true, - "default_variant": "Main", - "default_variants": ["Main"] + "default_variants": ["main"] }, "CreateUSD": { "enabled": false, - "default_variant": "Main", - "default_variants": ["Main"] + "default_variants": ["main"] }, "CreateUSDModel": { "enabled": false, - "default_variant": "Main", - "default_variants": ["Main"] + "default_variants": ["main"] }, "USDCreateShadingWorkspace": { "enabled": false, - "default_variant": "Main", - "default_variants": ["Main"] + "default_variants": ["main"] }, "CreateUSDRender": { "enabled": false, - "default_variant": "Main", - "default_variants": ["Main"] + "default_variants": ["main"] }, "CreateVDBCache": { "enabled": true, - "default_variant": "Main", - "default_variants": ["Main"] + "default_variants": ["main"] }, "CreateVrayROP": { "enabled": true, - "default_variant": "master", - "default_variants": ["master"] + "default_variants": ["main"] } }, "publish": { diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index f53501c7ac..e1c6d2d827 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -521,21 +521,18 @@ "enabled": true, "make_tx": true, "rs_tex": false, - "default_variant": "Main", "default_variants": [ "Main" ] }, "CreateRender": { "enabled": true, - "default_variant": "Main", "default_variants": [ "Main" ] }, "CreateUnrealStaticMesh": { "enabled": true, - "default_variant": "", "default_variants": [ "", "_Main" @@ -550,7 +547,6 @@ }, "CreateUnrealSkeletalMesh": { "enabled": true, - "default_variant": "", "default_variants": [], "joint_hints": "jnt_org" }, @@ -559,12 +555,10 @@ "publish_mip_map": true }, "CreateAnimation": { - "enabled": false, "write_color_sets": false, "write_face_sets": false, "include_parent_hierarchy": false, "include_user_defined_attributes": false, - "default_variant": "Main", "default_variants": [ "Main" ] @@ -573,7 +567,6 @@ "enabled": true, "write_color_sets": false, "write_face_sets": false, - "default_variant": "Main", "default_variants": [ "Main", "Proxy", @@ -585,7 +578,6 @@ "write_color_sets": false, "write_face_sets": false, "include_user_defined_attributes": false, - "default_variant": "Main", "default_variants": [ "Main" ] @@ -594,14 +586,12 @@ "enabled": true, "write_color_sets": false, "write_face_sets": false, - "default_variant": "Main", "default_variants": [ "Main" ] }, "CreateReview": { "enabled": true, - "default_variant": "Main", "default_variants": [ "Main" ], @@ -609,7 +599,6 @@ }, "CreateAss": { "enabled": true, - "default_variant": "Main", "default_variants": [ "Main" ], @@ -632,70 +621,60 @@ "enabled": true, "vrmesh": true, "alembic": true, - "default_variant": "Main", "default_variants": [ "Main" ] }, "CreateMultiverseUsd": { "enabled": true, - "default_variant": "Main", "default_variants": [ "Main" ] }, "CreateMultiverseUsdComp": { "enabled": true, - "default_variant": "Main", "default_variants": [ "Main" ] }, "CreateMultiverseUsdOver": { "enabled": true, - "default_variant": "Main", "default_variants": [ "Main" ] }, "CreateAssembly": { "enabled": true, - "default_variant": "Main", "default_variants": [ "Main" ] }, "CreateCamera": { "enabled": true, - "default_variant": "Main", "default_variants": [ "Main" ] }, "CreateLayout": { "enabled": true, - "default_variant": "Main", "default_variants": [ "Main" ] }, "CreateMayaScene": { "enabled": true, - "default_variant": "Main", "default_variants": [ "Main" ] }, "CreateRenderSetup": { "enabled": true, - "default_variant": "Main", "default_variants": [ "Main" ] }, "CreateRig": { "enabled": true, - "default_variant": "Main", "default_variants": [ "Main", "Sim", @@ -704,7 +683,6 @@ }, "CreateSetDress": { "enabled": true, - "default_variant": "Main", "default_variants": [ "Main", "Anim" @@ -712,14 +690,12 @@ }, "CreateVRayScene": { "enabled": true, - "default_variant": "Main", "default_variants": [ "Main" ] }, "CreateYetiRig": { "enabled": true, - "default_variant": "Main", "default_variants": [ "Main" ] diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_create.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_create.json index 6e1eaf7146..a1736c811d 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_create.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_create.json @@ -16,11 +16,6 @@ "key": "enabled", "label": "Enabled" }, - { - "type": "text", - "key": "default_variant", - "label": "Default variant" - }, { "type": "list", "key": "default_variants", diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/template_create_plugin.json b/openpype/settings/entities/schemas/projects_schema/schemas/template_create_plugin.json index 7384060625..3d2ed9f3d4 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/template_create_plugin.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/template_create_plugin.json @@ -11,11 +11,6 @@ "key": "enabled", "label": "Enabled" }, - { - "type": "text", - "key": "default_variant", - "label": "Default variant" - }, { "type": "list", "key": "default_variants", From d06b9dcdca4e6af00789a77806ed67f240bea4f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A0=20Serra=20Arrizabalaga?= Date: Tue, 8 Aug 2023 20:21:02 +0200 Subject: [PATCH 064/327] Remove fallback default_variant from Houdini base creator --- openpype/hosts/houdini/api/plugin.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/houdini/api/plugin.py b/openpype/hosts/houdini/api/plugin.py index 3d3b0e49b9..70c837205e 100644 --- a/openpype/hosts/houdini/api/plugin.py +++ b/openpype/hosts/houdini/api/plugin.py @@ -169,8 +169,6 @@ class HoudiniCreator(NewCreator, HoudiniCreatorBase): selected_nodes = [] settings_name = None - default_variant = "Main" - def create(self, subset_name, instance_data, pre_create_data): try: self.selected_nodes = [] From 0703c1c0d18206966cbb37885f9dc96b0e4336aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A0=20Serra=20Arrizabalaga?= Date: Tue, 8 Aug 2023 20:22:08 +0200 Subject: [PATCH 065/327] Remove default_variant from Maya schema --- .../schemas/projects_schema/schemas/schema_maya_create.json | 5 ----- 1 file changed, 5 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json index a8105bdb5d..8dec0a8817 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json @@ -387,11 +387,6 @@ "key": "alembic", "label": "Alembic" }, - { - "type": "text", - "key": "default_variant", - "label": "Default variant" - }, { "type": "list", "key": "default_variants", From 6edc471decafe71bc7f7eeb98580483596b2f5fb Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 9 Aug 2023 03:24:38 +0000 Subject: [PATCH 066/327] [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 12bff54676..393074c773 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.16.3-nightly.4" +__version__ = "3.16.3-nightly.5" From 28e75c8a4963dcf642a2a8d9e100f6e7e7200696 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 9 Aug 2023 03:25:19 +0000 Subject: [PATCH 067/327] 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 dea7e3c57f..5826d99d38 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.16.3-nightly.5 - 3.16.3-nightly.4 - 3.16.3-nightly.3 - 3.16.3-nightly.2 @@ -134,7 +135,6 @@ body: - 3.14.7-nightly.6 - 3.14.7-nightly.5 - 3.14.7-nightly.4 - - 3.14.7-nightly.3 validations: required: true - type: dropdown From 1e33e83de471d3866d194bfecf8a947dd1f1309b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A0=20Serra=20Arrizabalaga?= Date: Wed, 9 Aug 2023 10:29:52 +0200 Subject: [PATCH 068/327] Fix missed function interface change (#5430) Co-authored-by: Fabia Serra Arrizabalaga --- .../deadline/plugins/publish/submit_houdini_render_deadline.py | 2 +- .../modules/deadline/plugins/publish/submit_max_deadline.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py index 8c814bec95..108c377078 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -106,7 +106,7 @@ class HoudiniSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): job_info.EnvironmentKeyValue[key] = value # to recognize render jobs - job_info.add_render_job_env_var(job_info) + job_info.add_render_job_env_var() for i, filepath in enumerate(instance.data["files"]): dirname = os.path.dirname(filepath) diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index 2c1db1c880..8e05582962 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -132,7 +132,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, job_info.EnvironmentKeyValue[key] = value # to recognize render jobs - job_info.add_render_job_env_var(job_info) + job_info.add_render_job_env_var() job_info.EnvironmentKeyValue["OPENPYPE_LOG_NO_COLORS"] = "1" # Add list of expected files to job From 7493b6741788cf8113523e76c194a7e34382adb5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 9 Aug 2023 10:56:42 +0200 Subject: [PATCH 069/327] fix access to 'sync_server' attribute (#5431) --- openpype/tools/sceneinventory/view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/sceneinventory/view.py b/openpype/tools/sceneinventory/view.py index 816e396c08..af463e4867 100644 --- a/openpype/tools/sceneinventory/view.py +++ b/openpype/tools/sceneinventory/view.py @@ -55,7 +55,7 @@ class SceneInventoryView(QtWidgets.QTreeView): manager = ModulesManager() sync_server = manager.modules_by_name.get("sync_server") - sync_enabled = sync_server is not None and self.sync_server.enabled + sync_enabled = sync_server is not None and sync_server.enabled self.sync_server = sync_server self.sync_enabled = sync_enabled From 4043f8fed91d6b2cfce67a938c12450f84207559 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 9 Aug 2023 11:21:06 +0200 Subject: [PATCH 070/327] fixing slashes in condition --- openpype/hosts/nuke/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index ec1dd07ab4..c103a5d4cc 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2134,7 +2134,7 @@ class WorkfileSettings(object): current_ocio_path = current_ocio_path.replace("\\", "/") settings_ocio_path = settings_ocio_path.replace("\\", "/") - if current_ocio_path != config_data["path"]: + if current_ocio_path != settings_ocio_path: message = """ It seems like there's a mismatch between the OCIO config path set in your Nuke settings and the actual path set in your OCIO environment. From bc59808ef4dc536d80e11c91c803830f4a5c61d0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 9 Aug 2023 11:22:15 +0200 Subject: [PATCH 071/327] additional fix for dialogue --- openpype/hosts/nuke/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index c103a5d4cc..42e69c84b6 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2156,7 +2156,7 @@ Reopening Nuke should synchronize these paths and resolve any discrepancies. nuke.message( message.format( env_path=current_ocio_path, - settings_path=config_data["path"] + settings_path=settings_ocio_path ) ) return False From ef50ba5130d6be0b6f709ef60aa49181c67016a4 Mon Sep 17 00:00:00 2001 From: Fabia Serra Arrizabalaga Date: Wed, 9 Aug 2023 15:25:48 +0200 Subject: [PATCH 072/327] Remove schema setting changes from PR --- .../defaults/project_settings/houdini.json | 76 ++++-------- .../defaults/project_settings/maya.json | 26 ++-- .../schemas/schema_houdini_create.json | 113 +++++++----------- .../schemas/schema_maya_create_render.json | 2 +- .../schemas/template_create_plugin.json | 4 +- 5 files changed, 79 insertions(+), 142 deletions(-) diff --git a/openpype/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json index 512690bfd7..a5256aad8b 100644 --- a/openpype/settings/defaults/project_settings/houdini.json +++ b/openpype/settings/defaults/project_settings/houdini.json @@ -14,80 +14,48 @@ "create": { "CreateArnoldAss": { "enabled": true, - "default_variants": ["main"], + "default_variants": [], "ext": ".ass" }, - "CreateArnoldRop": { - "enabled": true, - "default_variants": ["main"] - }, "CreateAlembicCamera": { "enabled": true, - "default_variants": ["main"] - }, - "CreateBGEO": { - "enabled": true, - "default_variants": ["main"] + "defaults": [] }, "CreateCompositeSequence": { "enabled": true, - "default_variants": ["main"] - }, - "CreateHDA": { - "enabled": true, - "default_variants": ["main"] - }, - "CreateKarmaROP": { - "enabled": true, - "default_variants": ["main"] - }, - "CreateMantraROP": { - "enabled": true, - "default_variants": ["main"] + "defaults": [] }, "CreatePointCache": { "enabled": true, - "default_variants": ["main"] - }, - "CreateRedshiftProxy": { - "enabled": true, - "default_variants": ["main"] + "defaults": [] }, "CreateRedshiftROP": { "enabled": true, - "default_variants": ["main"] + "defaults": [] }, "CreateRemotePublish": { "enabled": true, - "default_variants": ["main"] - }, - "CreateReview": { - "enabled": true, - "default_variants": ["main"] - }, - "CreateUSD": { - "enabled": false, - "default_variants": ["main"] - }, - "CreateUSDModel": { - "enabled": false, - "default_variants": ["main"] - }, - "USDCreateShadingWorkspace": { - "enabled": false, - "default_variants": ["main"] - }, - "CreateUSDRender": { - "enabled": false, - "default_variants": ["main"] + "defaults": [] }, "CreateVDBCache": { "enabled": true, - "default_variants": ["main"] + "defaults": [] }, - "CreateVrayROP": { - "enabled": true, - "default_variants": ["main"] + "CreateUSD": { + "enabled": false, + "defaults": [] + }, + "CreateUSDModel": { + "enabled": false, + "defaults": [] + }, + "USDCreateShadingWorkspace": { + "enabled": false, + "defaults": [] + }, + "CreateUSDRender": { + "enabled": false, + "defaults": [] } }, "publish": { diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index e1c6d2d827..342d2bfb2a 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -527,7 +527,7 @@ }, "CreateRender": { "enabled": true, - "default_variants": [ + "defaults": [ "Main" ] }, @@ -627,55 +627,55 @@ }, "CreateMultiverseUsd": { "enabled": true, - "default_variants": [ + "defaults": [ "Main" ] }, "CreateMultiverseUsdComp": { "enabled": true, - "default_variants": [ + "defaults": [ "Main" ] }, "CreateMultiverseUsdOver": { "enabled": true, - "default_variants": [ + "defaults": [ "Main" ] }, "CreateAssembly": { "enabled": true, - "default_variants": [ + "defaults": [ "Main" ] }, "CreateCamera": { "enabled": true, - "default_variants": [ + "defaults": [ "Main" ] }, "CreateLayout": { "enabled": true, - "default_variants": [ + "defaults": [ "Main" ] }, "CreateMayaScene": { "enabled": true, - "default_variants": [ + "defaults": [ "Main" ] }, "CreateRenderSetup": { "enabled": true, - "default_variants": [ + "defaults": [ "Main" ] }, "CreateRig": { "enabled": true, - "default_variants": [ + "defaults": [ "Main", "Sim", "Cloth" @@ -683,20 +683,20 @@ }, "CreateSetDress": { "enabled": true, - "default_variants": [ + "defaults": [ "Main", "Anim" ] }, "CreateVRayScene": { "enabled": true, - "default_variants": [ + "defaults": [ "Main" ] }, "CreateYetiRig": { "enabled": true, - "default_variants": [ + "defaults": [ "Main" ] } diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_create.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_create.json index a1736c811d..4eb976d7b6 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_create.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_create.json @@ -1,83 +1,60 @@ { - "type": "dict", - "collapsible": true, - "key": "create", - "label": "Creator plugins", - "children": [ - { - "type": "dict", - "collapsible": true, - "key": "CreateArnoldAss", - "label": "Create Arnold Ass", - "checkbox_key": "enabled", - "children": [ - { - "type": "boolean", - "key": "enabled", - "label": "Enabled" - }, - { - "type": "list", - "key": "default_variants", - "label": "Default Subsets", - "object_type": "text" - }, - { - "type": "enum", - "key": "ext", - "label": "Default Output Format (extension)", - "multiselection": false, - "enum_items": [ - { - ".ass": ".ass" - }, - { - ".ass.gz": ".ass.gz (gzipped)" - } - ] - } - ] + "type": "dict", + "collapsible": true, + "key": "create", + "label": "Creator plugins", + "children": [ + { + "type": "dict", + "collapsible": true, + "key": "CreateArnoldAss", + "label": "Create Arnold Ass", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "list", + "key": "default_variants", + "label": "Default Subsets", + "object_type": "text" + }, + { + "type": "enum", + "key": "ext", + "label": "Default Output Format (extension)", + "multiselection": false, + "enum_items": [ + { + ".ass": ".ass" + }, + { + ".ass.gz": ".ass.gz (gzipped)" + } + ] + } + ] + }, { "type": "schema_template", "name": "template_create_plugin", "template_data": [ - { - "key": "CreateArnoldRop", - "label": "Create Arnold ROP" - }, { "key": "CreateAlembicCamera", "label": "Create Alembic Camera" }, - { - "key": "CreateBGEO", - "label": "Create Houdini BGEO" - }, { "key": "CreateCompositeSequence", "label": "Create Composite (Image Sequence)" }, - { - "key": "CreateKarmaROP", - "label": "Create Karma ROP" - }, - { - "key": "CreateMantraROP", - "label": "Create Mantra ROP" - }, - { - "key": "CreateHDA", - "label": "Create HDA" - }, { "key": "CreatePointCache", "label": "Create Point Cache" }, - { - "key": "CreateRedshiftProxy", - "label": "Create Redshift Proxy" - }, { "key": "CreateRedshiftROP", "label": "Create Redshift ROP" @@ -87,8 +64,8 @@ "label": "Create Remote Publish" }, { - "key": "CreateReview", - "label": "Create Review" + "key": "CreateVDBCache", + "label": "Create VDB Cache" }, { "key": "CreateUSD", @@ -105,14 +82,6 @@ { "key": "CreateUSDRender", "label": "Create USD Render" - }, - { - "key": "CreateVDBCache", - "label": "Create VDB Cache" - }, - { - "key": "CreateVrayROP", - "label": "Create VRay ROP" } ] } diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create_render.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create_render.json index 9d7432fe51..bc203a0514 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create_render.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create_render.json @@ -12,7 +12,7 @@ }, { "type": "list", - "key": "default_variants", + "key": "defaults", "label": "Default Subsets", "object_type": "text" } diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/template_create_plugin.json b/openpype/settings/entities/schemas/projects_schema/schemas/template_create_plugin.json index 3d2ed9f3d4..14d15e7840 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/template_create_plugin.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/template_create_plugin.json @@ -13,8 +13,8 @@ }, { "type": "list", - "key": "default_variants", - "label": "Default Variants", + "key": "defaults", + "label": "Default Subsets", "object_type": "text" } ] From a9fd8349fc27b4744b19d6a125a25440de973917 Mon Sep 17 00:00:00 2001 From: Fabia Serra Arrizabalaga Date: Wed, 9 Aug 2023 15:27:08 +0200 Subject: [PATCH 073/327] Remove whitespace differences --- .../schemas/schema_houdini_create.json | 170 +++++++++--------- 1 file changed, 85 insertions(+), 85 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_create.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_create.json index 4eb976d7b6..64d157d281 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_create.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_create.json @@ -1,89 +1,89 @@ { - "type": "dict", - "collapsible": true, - "key": "create", - "label": "Creator plugins", - "children": [ - { - "type": "dict", - "collapsible": true, - "key": "CreateArnoldAss", - "label": "Create Arnold Ass", - "checkbox_key": "enabled", - "children": [ - { - "type": "boolean", - "key": "enabled", - "label": "Enabled" - }, - { - "type": "list", - "key": "default_variants", - "label": "Default Subsets", - "object_type": "text" - }, - { - "type": "enum", - "key": "ext", - "label": "Default Output Format (extension)", - "multiselection": false, - "enum_items": [ - { - ".ass": ".ass" - }, - { - ".ass.gz": ".ass.gz (gzipped)" - } - ] - } - ] + "type": "dict", + "collapsible": true, + "key": "create", + "label": "Creator plugins", + "children": [ + { + "type": "dict", + "collapsible": true, + "key": "CreateArnoldAss", + "label": "Create Arnold Ass", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "list", + "key": "default_variants", + "label": "Default Subsets", + "object_type": "text" + }, + { + "type": "enum", + "key": "ext", + "label": "Default Output Format (extension)", + "multiselection": false, + "enum_items": [ + { + ".ass": ".ass" + }, + { + ".ass.gz": ".ass.gz (gzipped)" + } + ] + } + ] - }, - { - "type": "schema_template", - "name": "template_create_plugin", - "template_data": [ - { - "key": "CreateAlembicCamera", - "label": "Create Alembic Camera" }, - { - "key": "CreateCompositeSequence", - "label": "Create Composite (Image Sequence)" - }, - { - "key": "CreatePointCache", - "label": "Create Point Cache" - }, - { - "key": "CreateRedshiftROP", - "label": "Create Redshift ROP" - }, - { - "key": "CreateRemotePublish", - "label": "Create Remote Publish" - }, - { - "key": "CreateVDBCache", - "label": "Create VDB Cache" - }, - { - "key": "CreateUSD", - "label": "Create USD" - }, - { - "key": "CreateUSDModel", - "label": "Create USD Model" - }, - { - "key": "USDCreateShadingWorkspace", - "label": "Create USD Shading Workspace" - }, - { - "key": "CreateUSDRender", - "label": "Create USD Render" - } - ] - } - ] + { + "type": "schema_template", + "name": "template_create_plugin", + "template_data": [ + { + "key": "CreateAlembicCamera", + "label": "Create Alembic Camera" + }, + { + "key": "CreateCompositeSequence", + "label": "Create Composite (Image Sequence)" + }, + { + "key": "CreatePointCache", + "label": "Create Point Cache" + }, + { + "key": "CreateRedshiftROP", + "label": "Create Redshift ROP" + }, + { + "key": "CreateRemotePublish", + "label": "Create Remote Publish" + }, + { + "key": "CreateVDBCache", + "label": "Create VDB Cache" + }, + { + "key": "CreateUSD", + "label": "Create USD" + }, + { + "key": "CreateUSDModel", + "label": "Create USD Model" + }, + { + "key": "USDCreateShadingWorkspace", + "label": "Create USD Shading Workspace" + }, + { + "key": "CreateUSDRender", + "label": "Create USD Render" + } + ] + } + ] } From 08e86b50795a3b3cd9fbe5d9a8d8984233cce97e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 9 Aug 2023 15:37:31 +0200 Subject: [PATCH 074/327] nuke: review was not added to families --- .../nuke/plugins/publish/collect_nuke_instance_data.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/collect_nuke_instance_data.py b/openpype/hosts/nuke/plugins/publish/collect_nuke_instance_data.py index ffcf49d697..edd7a5cf27 100644 --- a/openpype/hosts/nuke/plugins/publish/collect_nuke_instance_data.py +++ b/openpype/hosts/nuke/plugins/publish/collect_nuke_instance_data.py @@ -43,13 +43,14 @@ class CollectNukeInstanceData(pyblish.api.InstancePlugin): }) - # add review family if review activated on instance - if instance.data.get("review"): - instance.data["families"].append("review") - # add creator attributes to instance creator_attributes = instance.data["creator_attributes"] instance.data.update(creator_attributes) + # add review family if review activated on instance + if instance.data.get("review"): + instance.data["families"].append("review") + + self.log.debug("Collected instance: {}".format( instance.data)) From 7b3d0b4f6d0686a44231d685da1fa70fe6e123d7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 9 Aug 2023 15:38:21 +0200 Subject: [PATCH 075/327] deadline validations are activated for nuke render --- .../deadline/plugins/publish/validate_deadline_connection.py | 2 +- .../modules/deadline/plugins/publish/validate_deadline_pools.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/validate_deadline_connection.py b/openpype/modules/deadline/plugins/publish/validate_deadline_connection.py index d5016a4d82..a30401e7dc 100644 --- a/openpype/modules/deadline/plugins/publish/validate_deadline_connection.py +++ b/openpype/modules/deadline/plugins/publish/validate_deadline_connection.py @@ -10,7 +10,7 @@ class ValidateDeadlineConnection(pyblish.api.InstancePlugin): label = "Validate Deadline Web Service" order = pyblish.api.ValidatorOrder hosts = ["maya", "nuke"] - families = ["renderlayer"] + families = ["renderlayer", "render"] def process(self, instance): # get default deadline webservice url from deadline module diff --git a/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py b/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py index e1c0595830..594f0ef866 100644 --- a/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py +++ b/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py @@ -19,6 +19,7 @@ class ValidateDeadlinePools(OptionalPyblishPluginMixin, order = pyblish.api.ValidatorOrder families = ["rendering", "render.farm", + "render.frames_farm", "renderFarm", "renderlayer", "maxrender"] From 84480649c8643589085d900e2f664457adfe2bb8 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 9 Aug 2023 15:45:13 +0200 Subject: [PATCH 076/327] nuke: implementing `frame_farm` target to deadline submitter --- .../plugins/publish/submit_nuke_deadline.py | 95 ++++++++++++------- 1 file changed, 60 insertions(+), 35 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py index 93c6ad8139..cfdeb4968b 100644 --- a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py @@ -90,7 +90,6 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin, if not instance.data.get("farm"): self.log.debug("Skipping local instance.") return - instance.data["attributeValues"] = self.get_attr_values_from_data( instance.data) @@ -123,13 +122,10 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin, render_path = instance.data['path'] script_path = context.data["currentFile"] - for item in context: - if "workfile" in item.data["families"]: - msg = "Workfile (scene) must be published along" - assert item.data["publish"] is True, msg - - template_data = item.data.get("anatomyData") - rep = item.data.get("representations")[0].get("name") + for item_ in context: + if "workfile" in item_.data["family"]: + template_data = item_.data.get("anatomyData") + rep = item_.data.get("representations")[0].get("name") template_data["representation"] = rep template_data["ext"] = rep template_data["comment"] = None @@ -141,19 +137,24 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin, "Using published scene for render {}".format(script_path) ) - response = self.payload_submit( - instance, - script_path, - render_path, - node.name(), - submit_frame_start, - submit_frame_end - ) - # Store output dir for unified publisher (filesequence) - instance.data["deadlineSubmissionJob"] = response.json() - instance.data["outputDir"] = os.path.dirname( - render_path).replace("\\", "/") - instance.data["publishJobState"] = "Suspended" + # only add main rendering job if target is not frames_farm + r_job_response_json = None + if instance.data["render_target"] != "frames_farm": + r_job_response = self.payload_submit( + instance, + script_path, + render_path, + node.name(), + submit_frame_start, + submit_frame_end + ) + r_job_response_json = r_job_response.json() + instance.data["deadlineSubmissionJob"] = r_job_response_json + + # Store output dir for unified publisher (filesequence) + instance.data["outputDir"] = os.path.dirname( + render_path).replace("\\", "/") + instance.data["publishJobState"] = "Suspended" if instance.data.get("bakingNukeScripts"): for baking_script in instance.data["bakingNukeScripts"]: @@ -161,18 +162,20 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin, script_path = baking_script["bakeScriptPath"] exe_node_name = baking_script["bakeWriteNodeName"] - resp = self.payload_submit( + b_job_response = self.payload_submit( instance, script_path, render_path, exe_node_name, submit_frame_start, submit_frame_end, - response.json() + r_job_response_json, + baking_submission=True ) # Store output dir for unified publisher (filesequence) - instance.data["deadlineSubmissionJob"] = resp.json() + instance.data["deadlineSubmissionJob"] = b_job_response.json() + instance.data["publishJobState"] = "Suspended" # add to list of job Id @@ -180,7 +183,7 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin, instance.data["bakingSubmissionJobs"] = [] instance.data["bakingSubmissionJobs"].append( - resp.json()["_id"]) + b_job_response.json()["_id"]) # redefinition of families if "render" in instance.data["family"]: @@ -199,15 +202,35 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin, exe_node_name, start_frame, end_frame, - response_data=None + response_data=None, + baking_submission=False, ): + """Submit payload to Deadline + + Args: + instance (pyblish.api.Instance): pyblish instance + script_path (str): path to nuke script + render_path (str): path to rendered images + exe_node_name (str): name of the node to render + start_frame (int): start frame + end_frame (int): end frame + response_data Optional[dict]: response data from + previous submission + baking_submission Optional[bool]: if it's baking submission + + Returns: + requests.Response + """ render_dir = os.path.normpath(os.path.dirname(render_path)) - batch_name = os.path.basename(script_path) - jobname = "%s - %s" % (batch_name, instance.name) + + # batch name + src_filepath = instance.context.data["currentFile"] + batch_name = os.path.basename(src_filepath) + job_name = os.path.basename(render_path) + if is_in_tests(): batch_name += datetime.now().strftime("%d%m%Y%H%M%S") - output_filename_0 = self.preview_fname(render_path) if not response_data: @@ -228,11 +251,8 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin, # Top-level group name "BatchName": batch_name, - # Asset dependency to wait for at least the scene file to sync. - # "AssetDependency0": script_path, - # Job name, as seen in Monitor - "Name": jobname, + "Name": job_name, # Arbitrary username, for visualisation in Monitor "UserName": self._deadline_user, @@ -294,12 +314,17 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin, "AuxFiles": [] } - if response_data.get("_id"): + # TODO: rewrite for baking with sequences + if baking_submission: payload["JobInfo"].update({ "JobType": "Normal", + "ChunkSize": 99999999 + }) + + if response_data.get("_id"): + payload["JobInfo"].update({ "BatchName": response_data["Props"]["Batch"], "JobDependency0": response_data["_id"], - "ChunkSize": 99999999 }) # Include critical environment variables with submission From 0c3fa2b61079e0497bccc9ae88e381f681afe39e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 9 Aug 2023 16:17:01 +0200 Subject: [PATCH 077/327] Unpack project: Fix import issue (#5433) * added 'load_json_file' to mongo init * add other missing imports --- openpype/client/mongo/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/client/mongo/__init__.py b/openpype/client/mongo/__init__.py index 5c5143a731..9f62d7a9cf 100644 --- a/openpype/client/mongo/__init__.py +++ b/openpype/client/mongo/__init__.py @@ -6,6 +6,9 @@ from .mongo import ( OpenPypeMongoConnection, get_project_database, get_project_connection, + load_json_file, + replace_project_documents, + store_project_documents, ) @@ -17,4 +20,7 @@ __all__ = ( "OpenPypeMongoConnection", "get_project_database", "get_project_connection", + "load_json_file", + "replace_project_documents", + "store_project_documents", ) From 42d766ed91f5a82e91d24ae4ca17bd5ed3c4f573 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 9 Aug 2023 16:38:31 +0200 Subject: [PATCH 078/327] nuke: collect slate fixed order --- .../nuke/plugins/publish/collect_slate_node.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/collect_slate_node.py b/openpype/hosts/nuke/plugins/publish/collect_slate_node.py index 5701087697..c7d65ffd24 100644 --- a/openpype/hosts/nuke/plugins/publish/collect_slate_node.py +++ b/openpype/hosts/nuke/plugins/publish/collect_slate_node.py @@ -5,7 +5,7 @@ import nuke class CollectSlate(pyblish.api.InstancePlugin): """Check if SLATE node is in scene and connected to rendering tree""" - order = pyblish.api.CollectorOrder + 0.09 + order = pyblish.api.CollectorOrder + 0.002 label = "Collect Slate Node" hosts = ["nuke"] families = ["render"] @@ -13,10 +13,14 @@ class CollectSlate(pyblish.api.InstancePlugin): def process(self, instance): node = instance.data["transientData"]["node"] - slate = next((n for n in nuke.allNodes() - if "slate" in n.name().lower() - if not n["disable"].getValue()), - None) + slate = next( + ( + n_ for n_ in nuke.allNodes() + if "slate" in n_.name().lower() + if not n_["disable"].getValue() + ), + None + ) if slate: # check if slate node is connected to write node tree From c8e66fd632323416a63c0c9d1bf248e516f2c2be Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 9 Aug 2023 20:34:05 +0300 Subject: [PATCH 079/327] resolve some conversations --- .../publish/validate_review_colorspace.py | 16 ++++++++-------- openpype/pipeline/colorspace.py | 4 +++- openpype/scripts/ocio_wrapper.py | 4 +++- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py index 67e29e0ee2..e493349946 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py +++ b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py @@ -4,6 +4,9 @@ from openpype.pipeline import PublishValidationError from openpype.pipeline.publish import RepairAction from openpype.hosts.houdini.api.action import SelectROPAction +import os +import hou + class SetDefaultViewSpaceAction(RepairAction): label = "Set default view space" @@ -32,8 +35,6 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance): - import hou # noqa - import os rop_node = hou.node(instance.data["instance_node"]) if os.getenv("OCIO") is None: @@ -70,16 +71,15 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin): used to set colorspace on opengl node to the default view. """ - import hou - from openpype.pipeline.colorspace import get_display_view_colorspace_name #noqa - from openpype.hosts.houdini.api.lib import get_color_management_preferences #noqa + from openpype.pipeline.colorspace import get_display_view_colorspace_name # noqa + from openpype.hosts.houdini.api.lib import get_color_management_preferences # noqa rop_node = hou.node(instance.data["instance_node"]) - data = get_color_management_preferences() + data = get_color_management_preferences() config_path = data.get("config") - display = data.get("display") - view = data.get("view") + display = data.get("display") + view = data.get("view") cls.log.debug("Get default view colorspace name..") diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 22e8175a7e..d84424270c 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -590,6 +590,7 @@ def _get_imageio_settings(project_settings, host_name): return imageio_global, imageio_host + def get_display_view_colorspace_name(config_path, display, view): """get view colorspace name for the given display and view. @@ -608,10 +609,11 @@ def get_display_view_colorspace_name(config_path, display, view): return get_display_view_colorspace_subprocess(config_path, display, view) - from openpype.scripts.ocio_wrapper import _get_display_view_colorspace_name #noqa + from openpype.scripts.ocio_wrapper import _get_display_view_colorspace_name # noqa return _get_display_view_colorspace_name(config_path, display, view) + def get_display_view_colorspace_subprocess(config_path, display, view): """get view colorspace name for the given display and view via subprocess. diff --git a/openpype/scripts/ocio_wrapper.py b/openpype/scripts/ocio_wrapper.py index f94faabe11..556568ce20 100644 --- a/openpype/scripts/ocio_wrapper.py +++ b/openpype/scripts/ocio_wrapper.py @@ -173,6 +173,7 @@ def _get_views_data(config_path): return data + def _get_display_view_colorspace_name(config_path, display, view): """get view colorspace name for the given display and view. @@ -199,6 +200,7 @@ def _get_display_view_colorspace_name(config_path, display, view): return colorspace + @config.command( name="get_display_view_colorspace_name", help=( @@ -223,7 +225,7 @@ def get_display_view_colorspace_name(in_path, out_path, display, view): """Aggregate view colorspace name to file. - Python 2 wrapped console command + Wrapper command for processes without acces to OpenColorIO Args: in_path (str): config file path string From 877facc5b89317a0fc541d2f06a9d9611c9e1acb Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 9 Aug 2023 21:56:42 +0300 Subject: [PATCH 080/327] set default colorspace on creation if there's OCIO --- .../houdini/plugins/create/create_review.py | 51 ++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/create/create_review.py b/openpype/hosts/houdini/plugins/create/create_review.py index ab06b30c35..797116aaca 100644 --- a/openpype/hosts/houdini/plugins/create/create_review.py +++ b/openpype/hosts/houdini/plugins/create/create_review.py @@ -3,6 +3,9 @@ from openpype.hosts.houdini.api import plugin from openpype.lib import EnumDef, BoolDef, NumberDef +import os +import hou + class CreateReview(plugin.HoudiniCreator): """Review with OpenGL ROP""" @@ -13,7 +16,6 @@ class CreateReview(plugin.HoudiniCreator): icon = "video-camera" def create(self, subset_name, instance_data, pre_create_data): - import hou instance_data.pop("active", None) instance_data.update({"node_type": "opengl"}) @@ -82,6 +84,10 @@ class CreateReview(plugin.HoudiniCreator): instance_node.setParms(parms) + # Set OCIO Colorspace to the default output colorspace + # if there's OCIO + self.set_colorcorrect_to_default_view_space(instance_node) + to_lock = ["id", "family"] self.lock_parameters(instance_node, to_lock) @@ -123,3 +129,46 @@ class CreateReview(plugin.HoudiniCreator): minimum=0.0001, decimals=3) ] + + def set_colorcorrect_to_default_view_space(self, + instance_node): + """Set ociocolorspace to the default output space.""" + + if os.getenv("OCIO") is None: + # No OCIO, skip setting ociocolorspace + return + + # if there's OCIO then set Color Correction parameter + # to OpenColorIO + instance_node.setParms({"colorcorrect": 2}) + + self.log.debug("Get default view colorspace name..") + + default_view_space = self.get_default_view_space() + instance_node.setParms( + {"ociocolorspace": default_view_space} + ) + + self.log.debug( + "'OCIO Colorspace' parm on '{}' has been set to " + "the default view color space '{}'" + .format(instance_node, default_view_space) + ) + + return default_view_space + + def get_default_view_space(self): + """Get default view space for ociocolorspace parm.""" + + from openpype.pipeline.colorspace import get_display_view_colorspace_name # noqa + from openpype.hosts.houdini.api.lib import get_color_management_preferences # noqa + + data = get_color_management_preferences() + config_path = data.get("config") + display = data.get("display") + view = data.get("view") + + default_view_space = get_display_view_colorspace_name(config_path, + display, view) + + return default_view_space From cff92425676acef62fc6b8489517e66286072925 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 9 Aug 2023 21:57:57 +0300 Subject: [PATCH 081/327] use .format instead of %s --- .../publish/validate_review_colorspace.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py index e493349946..5390b6b52f 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py +++ b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py @@ -49,17 +49,17 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin): # 'Color Correct' parm to be set to 'OpenColorIO' rop_node.setParms({"colorcorrect": 2}) cls.log.debug( - "'Color Correct' parm on '%s' has been set to" - " 'OpenColorIO'", rop_node + "'Color Correct' parm on '{}' has been set to" + " 'OpenColorIO'".format(rop_node) ) if rop_node.evalParm("ociocolorspace") not in \ hou.Color.ocio_spaces(): cls.log.error( - "'OCIO Colorspace' value on '%s' is not valid, " - "select a valid option from the dropdown menu.", - rop_node + "'OCIO Colorspace' value on '{}' is not valid, " + "select a valid option from the dropdown menu." + .format(rop_node) ) return rop_node @@ -88,7 +88,8 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin): rop_node.setParms({"ociocolorspace": default_view_space}) cls.log.debug( - "'OCIO Colorspace' parm on '%s' has been set to " - "the default view color space '%s'", - default_view_space, rop_node + "'OCIO Colorspace' parm on '{}' has been set to " + "the default view color space '{}'" + .formate(rop_node, default_view_space) + ) From 31969f394f9d3257845716f361a597a16363d813 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 10 Aug 2023 11:34:54 +0200 Subject: [PATCH 082/327] Settings: Houdini & Maya create plugin settings (#5436) * changed 'defaults' to 'default_variants' in create templates * use 'template_create_plugin' instead of 'schema_maya_create_render' * resave defaults and add Main to default value * updated AYON settings * formatting fixes * unified indentation * renamed 'Default Subsets' to 'Default Variants' --- .../defaults/project_settings/houdini.json | 44 ++++++--- .../defaults/project_settings/maya.json | 26 ++--- .../schemas/schema_houdini_create.json | 94 +++++++++---------- .../schemas/schema_maya_create.json | 32 ++++--- .../schemas/schema_maya_create_render.json | 20 ---- .../schemas/template_create_plugin.json | 4 +- .../server/settings/publish_plugins.py | 34 ++++--- server_addon/maya/server/settings/creators.py | 56 +++++------ 8 files changed, 163 insertions(+), 147 deletions(-) delete mode 100644 openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create_render.json diff --git a/openpype/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json index a5256aad8b..9d047c28bd 100644 --- a/openpype/settings/defaults/project_settings/houdini.json +++ b/openpype/settings/defaults/project_settings/houdini.json @@ -14,48 +14,70 @@ "create": { "CreateArnoldAss": { "enabled": true, - "default_variants": [], + "default_variants": [ + "Main" + ], "ext": ".ass" }, "CreateAlembicCamera": { "enabled": true, - "defaults": [] + "default_variants": [ + "Main" + ] }, "CreateCompositeSequence": { "enabled": true, - "defaults": [] + "default_variants": [ + "Main" + ] }, "CreatePointCache": { "enabled": true, - "defaults": [] + "default_variants": [ + "Main" + ] }, "CreateRedshiftROP": { "enabled": true, - "defaults": [] + "default_variants": [ + "Main" + ] }, "CreateRemotePublish": { "enabled": true, - "defaults": [] + "default_variants": [ + "Main" + ] }, "CreateVDBCache": { "enabled": true, - "defaults": [] + "default_variants": [ + "Main" + ] }, "CreateUSD": { "enabled": false, - "defaults": [] + "default_variants": [ + "Main" + ] }, "CreateUSDModel": { "enabled": false, - "defaults": [] + "default_variants": [ + "Main" + ] }, "USDCreateShadingWorkspace": { "enabled": false, - "defaults": [] + "default_variants": [ + "Main" + ] }, "CreateUSDRender": { "enabled": false, - "defaults": [] + "default_variants": [ + "Main" + ] } }, "publish": { diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 342d2bfb2a..e1c6d2d827 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -527,7 +527,7 @@ }, "CreateRender": { "enabled": true, - "defaults": [ + "default_variants": [ "Main" ] }, @@ -627,55 +627,55 @@ }, "CreateMultiverseUsd": { "enabled": true, - "defaults": [ + "default_variants": [ "Main" ] }, "CreateMultiverseUsdComp": { "enabled": true, - "defaults": [ + "default_variants": [ "Main" ] }, "CreateMultiverseUsdOver": { "enabled": true, - "defaults": [ + "default_variants": [ "Main" ] }, "CreateAssembly": { "enabled": true, - "defaults": [ + "default_variants": [ "Main" ] }, "CreateCamera": { "enabled": true, - "defaults": [ + "default_variants": [ "Main" ] }, "CreateLayout": { "enabled": true, - "defaults": [ + "default_variants": [ "Main" ] }, "CreateMayaScene": { "enabled": true, - "defaults": [ + "default_variants": [ "Main" ] }, "CreateRenderSetup": { "enabled": true, - "defaults": [ + "default_variants": [ "Main" ] }, "CreateRig": { "enabled": true, - "defaults": [ + "default_variants": [ "Main", "Sim", "Cloth" @@ -683,20 +683,20 @@ }, "CreateSetDress": { "enabled": true, - "defaults": [ + "default_variants": [ "Main", "Anim" ] }, "CreateVRayScene": { "enabled": true, - "defaults": [ + "default_variants": [ "Main" ] }, "CreateYetiRig": { "enabled": true, - "defaults": [ + "default_variants": [ "Main" ] } diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_create.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_create.json index 64d157d281..799bc0e81a 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_create.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_create.json @@ -19,7 +19,7 @@ { "type": "list", "key": "default_variants", - "label": "Default Subsets", + "label": "Default Variants", "object_type": "text" }, { @@ -39,51 +39,51 @@ ] }, - { - "type": "schema_template", - "name": "template_create_plugin", - "template_data": [ - { - "key": "CreateAlembicCamera", - "label": "Create Alembic Camera" - }, - { - "key": "CreateCompositeSequence", - "label": "Create Composite (Image Sequence)" - }, - { - "key": "CreatePointCache", - "label": "Create Point Cache" - }, - { - "key": "CreateRedshiftROP", - "label": "Create Redshift ROP" - }, - { - "key": "CreateRemotePublish", - "label": "Create Remote Publish" - }, - { - "key": "CreateVDBCache", - "label": "Create VDB Cache" - }, - { - "key": "CreateUSD", - "label": "Create USD" - }, - { - "key": "CreateUSDModel", - "label": "Create USD Model" - }, - { - "key": "USDCreateShadingWorkspace", - "label": "Create USD Shading Workspace" - }, - { - "key": "CreateUSDRender", - "label": "Create USD Render" - } - ] - } + { + "type": "schema_template", + "name": "template_create_plugin", + "template_data": [ + { + "key": "CreateAlembicCamera", + "label": "Create Alembic Camera" + }, + { + "key": "CreateCompositeSequence", + "label": "Create Composite (Image Sequence)" + }, + { + "key": "CreatePointCache", + "label": "Create Point Cache" + }, + { + "key": "CreateRedshiftROP", + "label": "Create Redshift ROP" + }, + { + "key": "CreateRemotePublish", + "label": "Create Remote Publish" + }, + { + "key": "CreateVDBCache", + "label": "Create VDB Cache" + }, + { + "key": "CreateUSD", + "label": "Create USD" + }, + { + "key": "CreateUSDModel", + "label": "Create USD Model" + }, + { + "key": "USDCreateShadingWorkspace", + "label": "Create USD Shading Workspace" + }, + { + "key": "CreateUSDRender", + "label": "Create USD Render" + } + ] + } ] } diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json index 8dec0a8817..b56e381c1d 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json @@ -29,14 +29,20 @@ { "type": "list", "key": "default_variants", - "label": "Default Subsets", + "label": "Default Variants", "object_type": "text" } ] }, - { - "type": "schema", - "name": "schema_maya_create_render" + { + "type": "schema_template", + "name": "template_create_plugin", + "template_data": [ + { + "key": "CreateRender", + "label": "Create Render" + } + ] }, { "type": "dict", @@ -53,7 +59,7 @@ { "type": "list", "key": "default_variants", - "label": "Default Subsets", + "label": "Default Variants", "object_type": "text" }, { @@ -85,7 +91,7 @@ { "type": "list", "key": "default_variants", - "label": "Default Subsets", + "label": "Default Variants", "object_type": "text" }, { @@ -148,7 +154,7 @@ { "type": "list", "key": "default_variants", - "label": "Default Subsets", + "label": "Default Variants", "object_type": "text" } ] @@ -178,7 +184,7 @@ { "type": "list", "key": "default_variants", - "label": "Default Subsets", + "label": "Default Variants", "object_type": "text" } ] @@ -213,7 +219,7 @@ { "type": "list", "key": "default_variants", - "label": "Default Subsets", + "label": "Default Variants", "object_type": "text" } ] @@ -243,7 +249,7 @@ { "type": "list", "key": "default_variants", - "label": "Default Subsets", + "label": "Default Variants", "object_type": "text" } ] @@ -263,7 +269,7 @@ { "type": "list", "key": "default_variants", - "label": "Default Subsets", + "label": "Default Variants", "object_type": "text" }, { @@ -288,7 +294,7 @@ { "type": "list", "key": "default_variants", - "label": "Default Subsets", + "label": "Default Variants", "object_type": "text" }, { @@ -390,7 +396,7 @@ { "type": "list", "key": "default_variants", - "label": "Default Subsets", + "label": "Default Variants", "object_type": "text" } ] diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create_render.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create_render.json deleted file mode 100644 index 68ad7ad63d..0000000000 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create_render.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "type": "dict", - "collapsible": true, - "key": "CreateRender", - "label": "Create Render", - "checkbox_key": "enabled", - "children": [ - { - "type": "boolean", - "key": "enabled", - "label": "Enabled" - }, - { - "type": "list", - "key": "defaults", - "label": "Default Subsets", - "object_type": "text" - } - ] -} \ No newline at end of file diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/template_create_plugin.json b/openpype/settings/entities/schemas/projects_schema/schemas/template_create_plugin.json index 14d15e7840..3d2ed9f3d4 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/template_create_plugin.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/template_create_plugin.json @@ -13,8 +13,8 @@ }, { "type": "list", - "key": "defaults", - "label": "Default Subsets", + "key": "default_variants", + "label": "Default Variants", "object_type": "text" } ] diff --git a/server_addon/houdini/server/settings/publish_plugins.py b/server_addon/houdini/server/settings/publish_plugins.py index 4155c75eb7..7d35d7e634 100644 --- a/server_addon/houdini/server/settings/publish_plugins.py +++ b/server_addon/houdini/server/settings/publish_plugins.py @@ -6,12 +6,18 @@ from ayon_server.settings import BaseSettingsModel # Creator Plugins class CreatorModel(BaseSettingsModel): enabled: bool = Field(title="Enabled") - defaults: list[str] = Field(title="Default Products") + default_variants: list[str] = Field( + title="Default Products", + default_factory=list, + ) class CreateArnoldAssModel(BaseSettingsModel): enabled: bool = Field(title="Enabled") - defaults: list[str] = Field(title="Default Products") + default_variants: list[str] = Field( + title="Default Products", + default_factory=list, + ) ext: str = Field(Title="Extension") @@ -54,49 +60,49 @@ class CreatePluginsModel(BaseSettingsModel): DEFAULT_HOUDINI_CREATE_SETTINGS = { "CreateArnoldAss": { "enabled": True, - "default_variants": [], + "default_variants": ["Main"], "ext": ".ass" }, "CreateAlembicCamera": { "enabled": True, - "defaults": [] + "default_variants": ["Main"] }, "CreateCompositeSequence": { "enabled": True, - "defaults": [] + "default_variants": ["Main"] }, "CreatePointCache": { "enabled": True, - "defaults": [] + "default_variants": ["Main"] }, "CreateRedshiftROP": { "enabled": True, - "defaults": [] + "default_variants": ["Main"] }, "CreateRemotePublish": { "enabled": True, - "defaults": [] + "default_variants": ["Main"] }, "CreateVDBCache": { "enabled": True, - "defaults": [] + "default_variants": ["Main"] }, "CreateUSD": { "enabled": False, - "defaults": [] + "default_variants": ["Main"] }, "CreateUSDModel": { "enabled": False, - "defaults": [] + "default_variants": ["Main"] }, "USDCreateShadingWorkspace": { "enabled": False, - "defaults": [] + "default_variants": ["Main"] }, "CreateUSDRender": { "enabled": False, - "defaults": [] - } + "default_variants": ["Main"] + }, } diff --git a/server_addon/maya/server/settings/creators.py b/server_addon/maya/server/settings/creators.py index 039b027898..9b97b92e59 100644 --- a/server_addon/maya/server/settings/creators.py +++ b/server_addon/maya/server/settings/creators.py @@ -7,14 +7,14 @@ class CreateLookModel(BaseSettingsModel): enabled: bool = Field(title="Enabled") make_tx: bool = Field(title="Make tx files") rs_tex: bool = Field(title="Make Redshift texture files") - defaults: list[str] = Field( - default_factory=["Main"], title="Default Products" + default_variants: list[str] = Field( + default_factory=list, title="Default Products" ) class BasicCreatorModel(BaseSettingsModel): enabled: bool = Field(title="Enabled") - defaults: list[str] = Field( + default_variants: list[str] = Field( default_factory=list, title="Default Products" ) @@ -22,20 +22,21 @@ class BasicCreatorModel(BaseSettingsModel): class CreateUnrealStaticMeshModel(BaseSettingsModel): enabled: bool = Field(title="Enabled") - defaults: list[str] = Field( - default_factory=["", "_Main"], + default_variants: list[str] = Field( + default_factory=list, title="Default Products" ) static_mesh_prefixes: str = Field("S", title="Static Mesh Prefix") collision_prefixes: list[str] = Field( - default_factory=["UBX", "UCP", "USP", "UCX"], + default_factory=list, title="Collision Prefixes" ) class CreateUnrealSkeletalMeshModel(BaseSettingsModel): enabled: bool = Field(title="Enabled") - defaults: list[str] = Field(default_factory=[], title="Default Products") + default_variants: list[str] = Field( + default_factory=list, title="Default Products") joint_hints: str = Field("jnt_org", title="Joint root hint") @@ -48,7 +49,7 @@ class BasicExportMeshModel(BaseSettingsModel): enabled: bool = Field(title="Enabled") write_color_sets: bool = Field(title="Write Color Sets") write_face_sets: bool = Field(title="Write Face Sets") - defaults: list[str] = Field( + default_variants: list[str] = Field( default_factory=list, title="Default Products" ) @@ -61,7 +62,7 @@ class CreateAnimationModel(BaseSettingsModel): title="Include Parent Hierarchy") include_user_defined_attributes: bool = Field( title="Include User Defined Attributes") - defaults: list[str] = Field( + default_variants: list[str] = Field( default_factory=list, title="Default Products" ) @@ -74,8 +75,8 @@ class CreatePointCacheModel(BaseSettingsModel): include_user_defined_attributes: bool = Field( title="Include User Defined Attributes" ) - defaults: list[str] = Field( - default_factory=["Main"], + default_variants: list[str] = Field( + default_factory=list, title="Default Products" ) @@ -84,8 +85,8 @@ class CreateProxyAlembicModel(BaseSettingsModel): enabled: bool = Field(title="Enabled") write_color_sets: bool = Field(title="Write Color Sets") write_face_sets: bool = Field(title="Write Face Sets") - defaults: list[str] = Field( - default_factory=["Main"], + default_variants: list[str] = Field( + default_factory=list, title="Default Products" ) @@ -115,7 +116,8 @@ class CreateVrayProxyModel(BaseSettingsModel): enabled: bool = Field(True) vrmesh: bool = Field(title="VrMesh") alembic: bool = Field(title="Alembic") - defaults: list[str] = Field(default_factory=list, title="Default Products") + default_variants: list[str] = Field( + default_factory=list, title="Default Products") class CreatorsModel(BaseSettingsModel): @@ -230,7 +232,7 @@ DEFAULT_CREATORS_SETTINGS = { }, "CreateRender": { "enabled": True, - "defaults": [ + "default_variants": [ "Main" ] }, @@ -295,19 +297,19 @@ DEFAULT_CREATORS_SETTINGS = { }, "CreateMultiverseUsd": { "enabled": True, - "defaults": [ + "default_variants": [ "Main" ] }, "CreateMultiverseUsdComp": { "enabled": True, - "defaults": [ + "default_variants": [ "Main" ] }, "CreateMultiverseUsdOver": { "enabled": True, - "defaults": [ + "default_variants": [ "Main" ] }, @@ -333,31 +335,31 @@ DEFAULT_CREATORS_SETTINGS = { }, "CreateAssembly": { "enabled": True, - "defaults": [ + "default_variants": [ "Main" ] }, "CreateCamera": { "enabled": True, - "defaults": [ + "default_variants": [ "Main" ] }, "CreateLayout": { "enabled": True, - "defaults": [ + "default_variants": [ "Main" ] }, "CreateMayaScene": { "enabled": True, - "defaults": [ + "default_variants": [ "Main" ] }, "CreateRenderSetup": { "enabled": True, - "defaults": [ + "default_variants": [ "Main" ] }, @@ -370,7 +372,7 @@ DEFAULT_CREATORS_SETTINGS = { }, "CreateRig": { "enabled": True, - "defaults": [ + "default_variants": [ "Main", "Sim", "Cloth" @@ -378,7 +380,7 @@ DEFAULT_CREATORS_SETTINGS = { }, "CreateSetDress": { "enabled": True, - "defaults": [ + "default_variants": [ "Main", "Anim" ] @@ -393,13 +395,13 @@ DEFAULT_CREATORS_SETTINGS = { }, "CreateVRayScene": { "enabled": True, - "defaults": [ + "default_variants": [ "Main" ] }, "CreateYetiRig": { "enabled": True, - "defaults": [ + "default_variants": [ "Main" ] } From 7973354fefc259c455bc8e61707147805a71d933 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Thu, 10 Aug 2023 12:31:49 +0100 Subject: [PATCH 083/327] Option to start versioning from 0 (#5262) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial version, replaced all hard 1 with 0 * ftrack v0 works only with version cast as str * workfile tools can set 0 * fixed hound stuff * fix for auto versioning not working anymore * fix for not incrementing version * hound fix * Settings determined versioning start * Code cosmetics * Better failsafe for collecting settings. * Initial profiles commit * Hound * Working profiles * Update openpype/hosts/webpublisher/plugins/publish/collect_published_files.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * Update openpype/hosts/webpublisher/plugins/publish/collect_published_files.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * Update openpype/plugins/publish/collect_anatomy_instance_data.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * Update openpype/settings/entities/schemas/projects_schema/schema_project_global.json Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * Illicitit feedback * Update openpype/pipeline/context_tools.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * Fix collect_published_files * Working version * Hound * Update openpype/pipeline/version_start.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * Update openpype/pipeline/version_start.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * Update openpype/tools/push_to_project/control_integrate.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * Update openpype/hosts/photoshop/plugins/publish/collect_published_version.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * Update openpype/hosts/photoshop/plugins/publish/collect_published_version.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * Update openpype/hosts/webpublisher/plugins/publish/collect_published_files.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * Update openpype/hosts/webpublisher/plugins/publish/collect_published_files.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * Update openpype/hosts/webpublisher/plugins/publish/collect_published_files.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * Update openpype/pipeline/workfile/path_resolving.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * Update openpype/settings/__init__.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * Hound * Illicitit feedback * Replace host.name * Update openpype/plugins/publish/collect_anatomy_instance_data.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * reuse 'task_name' and 'task_type' * skip hero integration when source version in 0 --------- Co-authored-by: maxpareschi Co-authored-by: Jakub Ježek Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Co-authored-by: Jakub Trllo --- .../publish/collect_published_version.py | 13 ++++- .../tvpaint/plugins/load/load_workfile.py | 9 ++- .../publish/collect_published_files.py | 39 +++++++++++-- .../plugins/publish/submit_publish_job.py | 13 ++++- .../plugins/publish/integrate_ftrack_api.py | 2 - openpype/pipeline/__init__.py | 2 +- openpype/pipeline/context_tools.py | 2 +- openpype/pipeline/version_start.py | 37 +++++++++++++ openpype/pipeline/workfile/path_resolving.py | 10 +++- .../publish/collect_anatomy_instance_data.py | 29 +++++++--- .../plugins/publish/integrate_hero_version.py | 6 ++ openpype/scripts/fusion_switch_shot.py | 12 ++-- .../defaults/project_settings/global.json | 3 + .../schema_project_global.json | 55 +++++++++++++++++++ .../push_to_project/control_integrate.py | 21 +++---- .../widgets/widget_family.py | 11 +++- openpype/tools/workfiles/save_as_dialog.py | 19 ++++++- 17 files changed, 239 insertions(+), 44 deletions(-) create mode 100644 openpype/pipeline/version_start.py diff --git a/openpype/hosts/photoshop/plugins/publish/collect_published_version.py b/openpype/hosts/photoshop/plugins/publish/collect_published_version.py index 7371c0564f..eec6f1fae4 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_published_version.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_published_version.py @@ -18,6 +18,7 @@ Provides: import pyblish.api from openpype.client import get_last_version_by_subset_name +from openpype.pipeline.version_start import get_versioning_start class CollectPublishedVersion(pyblish.api.ContextPlugin): @@ -47,9 +48,17 @@ class CollectPublishedVersion(pyblish.api.ContextPlugin): version_doc = get_last_version_by_subset_name(project_name, workfile_subset_name, asset_id) - version_int = 1 + if version_doc: - version_int += int(version_doc["name"]) + version_int = int(version_doc["name"]) + 1 + else: + version_int = get_versioning_start( + project_name, + "photoshop", + task_name=context.data["task"], + task_type=context.data["taskType"], + project_settings=context.data["project_settings"] + ) self.log.debug(f"Setting {version_int} to context.") context.data["version"] = version_int diff --git a/openpype/hosts/tvpaint/plugins/load/load_workfile.py b/openpype/hosts/tvpaint/plugins/load/load_workfile.py index 2155a1bbd5..169bfdcdd8 100644 --- a/openpype/hosts/tvpaint/plugins/load/load_workfile.py +++ b/openpype/hosts/tvpaint/plugins/load/load_workfile.py @@ -18,6 +18,7 @@ from openpype.hosts.tvpaint.api.lib import ( from openpype.hosts.tvpaint.api.pipeline import ( get_current_workfile_context, ) +from openpype.pipeline.version_start import get_versioning_start class LoadWorkfile(plugin.Loader): @@ -95,7 +96,13 @@ class LoadWorkfile(plugin.Loader): )[1] if version is None: - version = 1 + version = get_versioning_start( + project_name, + "tvpaint", + task_name=task_name, + task_type=data["task"]["type"], + family="workfile" + ) else: version += 1 diff --git a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py index 79ed499a20..1416255083 100644 --- a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py +++ b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py @@ -25,6 +25,7 @@ from openpype.lib import ( ) from openpype.pipeline.create import get_subset_name from openpype_modules.webpublisher.lib import parse_json +from openpype.pipeline.version_start import get_versioning_start class CollectPublishedFiles(pyblish.api.ContextPlugin): @@ -103,7 +104,13 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin): project_settings=context.data["project_settings"] ) version = self._get_next_version( - project_name, asset_doc, subset_name + project_name, + asset_doc, + task_name, + task_type, + family, + subset_name, + context ) next_versions.append(version) @@ -141,8 +148,9 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin): try: no_of_frames = self._get_number_of_frames(file_url) if no_of_frames: - frame_end = int(frame_start) + \ - math.ceil(no_of_frames) + frame_end = ( + int(frame_start) + math.ceil(no_of_frames) + ) frame_end = math.ceil(frame_end) - 1 instance.data["frameEnd"] = frame_end self.log.debug("frameEnd:: {}".format( @@ -270,7 +278,16 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin): config["families"], config["tags"]) - def _get_next_version(self, project_name, asset_doc, subset_name): + def _get_next_version( + self, + project_name, + asset_doc, + task_name, + task_type, + family, + subset_name, + context + ): """Returns version number or 1 for 'asset' and 'subset'""" version_doc = get_last_version_by_subset_name( @@ -279,9 +296,19 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin): asset_doc["_id"], fields=["name"] ) - version = 1 if version_doc: - version += int(version_doc["name"]) + version = int(version_doc["name"]) + 1 + else: + version = get_versioning_start( + project_name, + "webpublisher", + task_name=task_name, + task_type=task_type, + family=family, + subset=subset_name, + project_settings=context.data["project_settings"] + ) + return version def _get_number_of_frames(self, file_url): diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index ec182fcd66..5e8c005d07 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -3,7 +3,7 @@ import os import json import re -from copy import copy, deepcopy +from copy import deepcopy import requests import clique @@ -16,6 +16,7 @@ from openpype.client import ( from openpype.pipeline import publish, legacy_io from openpype.lib import EnumDef, is_running_from_build from openpype.tests.lib import is_in_tests +from openpype.pipeline.version_start import get_versioning_start from openpype.pipeline.farm.pyblish_functions import ( create_skeleton_instance, @@ -566,7 +567,15 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, if version: version = int(version["name"]) + 1 else: - version = 1 + version = get_versioning_start( + project_name, + template_data["app"], + task_name=template_data["task"]["name"], + task_type=template_data["task"]["type"], + family="render", + subset=subset, + project_settings=context.data["project_settings"] + ) host_name = context.data["hostName"] task_info = template_data.get("task") or {} diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py index deb8b414f0..6ca5d1d4ef 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py @@ -11,10 +11,8 @@ Provides: """ import os -import sys import collections -import six import pyblish.api import clique diff --git a/openpype/pipeline/__init__.py b/openpype/pipeline/__init__.py index 59f1655f91..8f370d389b 100644 --- a/openpype/pipeline/__init__.py +++ b/openpype/pipeline/__init__.py @@ -94,7 +94,7 @@ from .context_tools import ( get_current_host_name, get_current_project_name, get_current_asset_name, - get_current_task_name, + get_current_task_name ) install = install_host uninstall = uninstall_host diff --git a/openpype/pipeline/context_tools.py b/openpype/pipeline/context_tools.py index c12b76cc74..9ada2d42a4 100644 --- a/openpype/pipeline/context_tools.py +++ b/openpype/pipeline/context_tools.py @@ -35,7 +35,7 @@ from . import ( register_inventory_action_path, register_creator_plugin_path, deregister_loader_plugin_path, - deregister_inventory_action_path, + deregister_inventory_action_path ) diff --git a/openpype/pipeline/version_start.py b/openpype/pipeline/version_start.py new file mode 100644 index 0000000000..0240ab0c7a --- /dev/null +++ b/openpype/pipeline/version_start.py @@ -0,0 +1,37 @@ +from openpype.lib.profiles_filtering import filter_profiles +from openpype.settings import get_project_settings + + +def get_versioning_start( + project_name, + host_name, + task_name=None, + task_type=None, + family=None, + subset=None, + project_settings=None, +): + """Get anatomy versioning start""" + if not project_settings: + project_settings = get_project_settings(project_name) + + version_start = 1 + settings = project_settings["global"] + profiles = settings.get("version_start_category", {}).get("profiles", []) + + if not profiles: + return version_start + + filtering_criteria = { + "host_names": host_name, + "families": family, + "task_names": task_name, + "task_types": task_type, + "subsets": subset + } + profile = filter_profiles(profiles, filtering_criteria) + + if profile is None: + return version_start + + return profile["version_start"] diff --git a/openpype/pipeline/workfile/path_resolving.py b/openpype/pipeline/workfile/path_resolving.py index 15689f4d99..78acee20da 100644 --- a/openpype/pipeline/workfile/path_resolving.py +++ b/openpype/pipeline/workfile/path_resolving.py @@ -10,7 +10,7 @@ from openpype.lib import ( Logger, StringTemplate, ) -from openpype.pipeline import Anatomy +from openpype.pipeline import version_start, Anatomy from openpype.pipeline.template_data import get_template_data @@ -316,7 +316,13 @@ def get_last_workfile( ) if filename is None: data = copy.deepcopy(fill_data) - data["version"] = 1 + data["version"] = version_start.get_versioning_start( + data["project"]["name"], + data["app"], + task_name=data["task"]["name"], + task_type=data["task"]["type"], + family="workfile" + ) data.pop("comment", None) if not data.get("ext"): data["ext"] = extensions[0] diff --git a/openpype/plugins/publish/collect_anatomy_instance_data.py b/openpype/plugins/publish/collect_anatomy_instance_data.py index 128ad90b4f..ef8f4af8fb 100644 --- a/openpype/plugins/publish/collect_anatomy_instance_data.py +++ b/openpype/plugins/publish/collect_anatomy_instance_data.py @@ -32,6 +32,7 @@ from openpype.client import ( get_subsets, get_last_versions ) +from openpype.pipeline.version_start import get_versioning_start class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): @@ -191,15 +192,6 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): version_number = context.data('version') else: version_number = instance.data.get("version") - # If version is not specified for instance or context - if version_number is None: - # TODO we should be able to change default version by studio - # preferences (like start with version number `0`) - version_number = 1 - # use latest version (+1) if already any exist - latest_version = instance.data["latestVersion"] - if latest_version is not None: - version_number += int(latest_version) anatomy_updates = { "asset": instance.data["asset"], @@ -225,6 +217,7 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): anatomy_updates["parent"] = parent_name # Task + task_type = None task_name = instance.data.get("task") if task_name: asset_tasks = asset_doc["data"]["tasks"] @@ -240,6 +233,24 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): "short": task_code } + # Define version + # use latest version (+1) if already any exist + if version_number is None: + latest_version = instance.data["latestVersion"] + if latest_version is not None: + version_number = int(latest_version) + 1 + + # If version is not specified for instance or context + if version_number is None: + version_number = get_versioning_start( + context.data["projectName"], + instance.context.data["hostName"], + task_name=task_name, + task_type=task_type, + family=instance.data["family"], + subset=instance.data["subset"] + ) + # Additional data resolution_width = instance.data.get("resolutionWidth") if resolution_width: diff --git a/openpype/plugins/publish/integrate_hero_version.py b/openpype/plugins/publish/integrate_hero_version.py index b7feeac6a4..6c21664b78 100644 --- a/openpype/plugins/publish/integrate_hero_version.py +++ b/openpype/plugins/publish/integrate_hero_version.py @@ -142,6 +142,12 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): )) return + if AYON_SERVER_ENABLED and src_version_entity["name"] == 0: + self.log.debug( + "Version 0 cannot have hero version. Skipping." + ) + return + all_copied_files = [] transfers = instance.data.get("transfers", list()) for _src, dst in transfers: diff --git a/openpype/scripts/fusion_switch_shot.py b/openpype/scripts/fusion_switch_shot.py index 8ecf4fb5ea..1cc728226f 100644 --- a/openpype/scripts/fusion_switch_shot.py +++ b/openpype/scripts/fusion_switch_shot.py @@ -19,6 +19,7 @@ from openpype.pipeline import ( ) from openpype.pipeline.context_tools import get_workdir_from_session +from openpype.pipeline.version_start import get_versioning_start log = logging.getLogger("Update Slap Comp") @@ -26,9 +27,6 @@ log = logging.getLogger("Update Slap Comp") def _format_version_folder(folder): """Format a version folder based on the filepath - Assumption here is made that, if the path does not exists the folder - will be "v001" - Args: folder: file path to a folder @@ -36,9 +34,13 @@ def _format_version_folder(folder): str: new version folder name """ - new_version = 1 + new_version = get_versioning_start( + get_current_project_name(), + "fusion", + family="workfile" + ) if os.path.isdir(folder): - re_version = re.compile("v\d+$") + re_version = re.compile(r"v\d+$") versions = [i for i in os.listdir(folder) if os.path.isdir(i) and re_version.match(i)] if versions: diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index b6eb2f52f1..06a595d1c5 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -1,4 +1,7 @@ { + "version_start_category": { + "profiles": [] + }, "imageio": { "activate_global_color_management": false, "ocio_config": { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_global.json b/openpype/settings/entities/schemas/projects_schema/schema_project_global.json index 953361935c..4094632c72 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_global.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_global.json @@ -5,6 +5,61 @@ "label": "Global", "is_file": true, "children": [ + { + "type": "dict", + "key": "version_start_category", + "label": "Version Start", + "collapsible": true, + "collapsible_key": true, + "children": [ + { + "type": "list", + "collapsible": true, + "key": "profiles", + "label": "Profiles", + "object_type": { + "type": "dict", + "children": [ + { + "key": "host_names", + "label": "Host names", + "type": "hosts-enum", + "multiselection": true + }, + { + "key": "task_types", + "label": "Task types", + "type": "task-types-enum" + }, + { + "key": "task_names", + "label": "Task names", + "type": "list", + "object_type": "text" + }, + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "text" + }, + { + "key": "subsets", + "label": "Subset names", + "type": "list", + "object_type": "text" + }, + { + "key": "version_start", + "label": "Version Start", + "type": "number", + "minimum": 0 + } + ] + } + } + ] + }, { "key": "imageio", "type": "dict", diff --git a/openpype/tools/push_to_project/control_integrate.py b/openpype/tools/push_to_project/control_integrate.py index 37a0512d59..a822339ccf 100644 --- a/openpype/tools/push_to_project/control_integrate.py +++ b/openpype/tools/push_to_project/control_integrate.py @@ -40,6 +40,7 @@ from openpype.lib import ( from openpype.lib.file_transaction import FileTransaction from openpype.settings import get_project_settings from openpype.pipeline import Anatomy +from openpype.pipeline.version_start import get_versioning_start from openpype.pipeline.template_data import get_template_data from openpype.pipeline.publish import get_publish_template_name from openpype.pipeline.create import get_subset_name @@ -940,9 +941,17 @@ class ProjectPushItemProcess: last_version_doc = get_last_version_by_subset_id( project_name, subset_id ) - version = 1 if last_version_doc: - version += int(last_version_doc["name"]) + version = int(last_version_doc["name"]) + 1 + else: + version = get_versioning_start( + project_name, + self.host_name, + task_name=self.task_info["name"], + task_type=self.task_info["type"], + family=families[0], + subset=subset_doc["name"] + ) existing_version_doc = get_version_by_name( project_name, version, subset_id @@ -966,14 +975,6 @@ class ProjectPushItemProcess: return - if version is None: - last_version_doc = get_last_version_by_subset_id( - project_name, subset_id - ) - version = 1 - if last_version_doc: - version += int(last_version_doc["name"]) - version_doc = new_version_doc( version, subset_id, version_data ) diff --git a/openpype/tools/standalonepublish/widgets/widget_family.py b/openpype/tools/standalonepublish/widgets/widget_family.py index 8c18a93a00..73dc2122db 100644 --- a/openpype/tools/standalonepublish/widgets/widget_family.py +++ b/openpype/tools/standalonepublish/widgets/widget_family.py @@ -10,6 +10,7 @@ from openpype.client import ( ) from openpype.settings import get_project_settings from openpype.pipeline import LegacyCreator +from openpype.pipeline.version_start import get_versioning_start from openpype.pipeline.create import ( SUBSET_NAME_ALLOWED_SYMBOLS, TaskNotSetError, @@ -299,7 +300,15 @@ class FamilyWidget(QtWidgets.QWidget): project_name = self.dbcon.active_project() asset_name = self.asset_name subset_name = self.input_result.text() - version = 1 + plugin = self.list_families.currentItem().data(PluginRole) + family = plugin.family.rsplit(".", 1)[-1] + version = get_versioning_start( + project_name, + "standalonepublisher", + task_name=self.dbcon.Session["AVALON_TASK"], + family=family, + subset=subset_name + ) asset_doc = None subset_doc = None diff --git a/openpype/tools/workfiles/save_as_dialog.py b/openpype/tools/workfiles/save_as_dialog.py index 9f1d1060da..7052eaed06 100644 --- a/openpype/tools/workfiles/save_as_dialog.py +++ b/openpype/tools/workfiles/save_as_dialog.py @@ -12,6 +12,7 @@ from openpype.pipeline import ( from openpype.pipeline.workfile import get_last_workfile_with_version from openpype.pipeline.template_data import get_template_data_with_names from openpype.tools.utils import PlaceholderLineEdit +from openpype.pipeline import version_start, get_current_host_name log = logging.getLogger(__name__) @@ -218,7 +219,15 @@ class SaveAsDialog(QtWidgets.QDialog): # Version number input version_input = QtWidgets.QSpinBox(version_widget) - version_input.setMinimum(1) + version_input.setMinimum( + version_start.get_versioning_start( + self.data["project"]["name"], + get_current_host_name(), + task_name=self.data["task"]["name"], + task_type=self.data["task"]["type"], + family="workfile" + ) + ) version_input.setMaximum(9999) # Last version checkbox @@ -420,7 +429,13 @@ class SaveAsDialog(QtWidgets.QDialog): )[1] if version is None: - version = 1 + version = version_start.get_versioning_start( + data["project"]["name"], + get_current_host_name(), + task_name=self.data["task"]["name"], + task_type=self.data["task"]["type"], + family="workfile" + ) else: version += 1 From 745aacea0c7db07b0da408af990a480dff45bc31 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 10 Aug 2023 16:42:29 +0200 Subject: [PATCH 084/327] Chore: Versions post fixes (#5441) * fix how version definition order * added 'folder' to anatomy data --- .../publish/collect_anatomy_instance_data.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/openpype/plugins/publish/collect_anatomy_instance_data.py b/openpype/plugins/publish/collect_anatomy_instance_data.py index ef8f4af8fb..b4f4d6a16a 100644 --- a/openpype/plugins/publish/collect_anatomy_instance_data.py +++ b/openpype/plugins/publish/collect_anatomy_instance_data.py @@ -188,16 +188,13 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): project_task_types = project_doc["config"]["tasks"] for instance in context: - if self.follow_workfile_version: - version_number = context.data('version') - else: - version_number = instance.data.get("version") - anatomy_updates = { "asset": instance.data["asset"], + "folder": { + "name": instance.data["asset"], + }, "family": instance.data["family"], "subset": instance.data["subset"], - "version": version_number } # Hierarchy @@ -234,6 +231,11 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): } # Define version + if self.follow_workfile_version: + version_number = context.data('version') + else: + version_number = instance.data.get("version") + # use latest version (+1) if already any exist if version_number is None: latest_version = instance.data["latestVersion"] @@ -250,6 +252,7 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): family=instance.data["family"], subset=instance.data["subset"] ) + anatomy_updates["version"] = version_number # Additional data resolution_width = instance.data.get("resolutionWidth") From 4013148167783590d62e1a6d6882c2d07ada2d65 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 11 Aug 2023 15:29:36 +0800 Subject: [PATCH 085/327] name of the read node should be updated correctly when setting versions and switching assets --- openpype/hosts/nuke/plugins/load/load_image.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/openpype/hosts/nuke/plugins/load/load_image.py b/openpype/hosts/nuke/plugins/load/load_image.py index d8c0a82206..225365056a 100644 --- a/openpype/hosts/nuke/plugins/load/load_image.py +++ b/openpype/hosts/nuke/plugins/load/load_image.py @@ -212,6 +212,8 @@ class LoadImage(load.LoaderPlugin): last = first = int(frame_number) # Set the global in to the start frame of the sequence + read_name = self._get_node_name(representation) + node["name"].setValue(read_name) node["file"].setValue(file) node["origfirst"].setValue(first) node["first"].setValue(first) @@ -250,3 +252,17 @@ class LoadImage(load.LoaderPlugin): with viewer_update_and_undo_stop(): nuke.delete(node) + + def _get_node_name(self, representation): + + repre_cont = representation["context"] + name_data = { + "asset": repre_cont["asset"], + "subset": repre_cont["subset"], + "representation": representation["name"], + "ext": repre_cont["representation"], + "id": representation["_id"], + "class_name": self.__class__.__name__ + } + + return self.node_name_template.format(**name_data) From 7b2de9248e795d6a5f1ec5014f9057a8b8b2d070 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Fri, 11 Aug 2023 08:34:33 +0000 Subject: [PATCH 086/327] [Automated] Release --- CHANGELOG.md | 832 ++++++++++++++++++++++++++++++++++++++++++++ openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 834 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2930d45eb..c6d8f01234 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,838 @@ # Changelog +## [3.16.3](https://github.com/ynput/OpenPype/tree/3.16.3) + + +[Full Changelog](https://github.com/ynput/OpenPype/compare/3.16.2...3.16.3) + +### **🆕 New features** + + +
+AYON: 3rd party addon usage #5300 + +Prepare OpenPype code to be able use `ayon-third-party` addon which supply ffmpeg and OpenImageIO executables. Because they both can support to define custom arguments (more than one) a new functions were needed to supply.New functions are `get_ffmpeg_tool_args` and `get_oiio_tool_args`. They work similar to previous but instead of string are returning list of strings. All places using previous functions `get_ffmpeg_tool_path` and `get_oiio_tool_path` are now using new ones. They should be backwards compatible and even with addon if returns single argument. + + +___ + +
+ + +
+AYON: Addon settings in OpenPype #5347 + +Moved settings addons to OpenPype server addon. Modified create package to create zip files for server for each settings addon and for openpype addon. + + +___ + +
+ + +
+AYON: Add folder to template data #5417 + +Added `folder` to template data, so `{folder[name]}` can be used in templates. + + +___ + +
+ + +
+Option to start versioning from 0 #5262 + +This PR adds a settings option to start all versioning from 0.This PR will replace #4455. + + +___ + +
+ + +
+Ayon: deadline implementation #5321 + +Quick implementation of deadline in Ayon. New Ayon plugin added for Deadline repository + + +___ + +
+ + +
+AYON: Remove AYON launch logic from OpenPype #5348 + +Removed AYON launch logic from OpenPype. The logic is outdated at this moment and is replaced by `ayon-launcher`. + + +___ + +
+ +### **🚀 Enhancements** + + +
+Bug: Error on multiple instance rig with maya #5310 + +I change endswith method by startswith method because the set are automacaly name out_SET, out_SET1, out_SET2 ... + + +___ + +
+ + +
+Applications: Use prelaunch hooks to extract environments #5387 + +Environment variable preparation is based on prelaunch hooks. This should allow to pass OCIO environment variables to farm jobs. + + +___ + +
+ + +
+Applications: Launch hooks cleanup #5395 + +Use `set` instead of `list` for filtering attributes in launch hooks. Celaction hooks dir does not contain `__init__.py`. Celaction prelaunch hook is reusing `CELACTION_ROOT_DIR`. Launch hooks are using full import from `openpype.lib.applications`. + + +___ + +
+ + +
+Applications: Environment variables order #5245 + +Changed order of set environment variables. First are set context environment variables and then project environment overrides. Also asset and task environemnt variables are optional. + + +___ + +
+ + +
+Autosave preferences can be read after Nuke opens the script #5295 + +Looks like I need to open the script in Nuke to be able to correctly load the autosave preferences.This PR reads the Nuke script in context, and offers owerwriting the current script with autosaved one if autosave exists. + + +___ + +
+ + +
+Resolve: Update with compatible resolve version and latest docs #5317 + +Missing information about compatible Resolve version and latest docs from https://github.com/ynput/OpenPype/tree/develop/openpype/hosts/resolve + + +___ + +
+ + +
+Chore: Remove deprecated functions #5323 + +Removed functions/classes that are deprecated and marked to be removed. + + +___ + +
+ + +
+Nuke Render and Prerender nodes Process Order - OP-3555 #5332 + +This PR exposes control over the order of processing of the instances, by sorting the instances created. The sorting happens on the `render_order` and subset name. If the knob `render_order` is found on the instance, we'll sort by that first before sorting by subset name.`render_order` instances are processed before nodes without `render_order`. This could be extended in the future by querying other knobs but I dont know of a usecase for this.Hardcoded the creator `order` attribute of the `prerender` class to be before the `render`. Could be exposed to the user/studio but dont know of a use case for this. + + +___ + +
+ + +
+Unreal: Python Environment Improvements #5344 + +Automatically set `UE_PYTHONPATH` as `PYTHONPATH` when launching Unreal. + + +___ + +
+ + +
+Unreal: Custom location for Unreal Ayon Plugin #5346 + +Added a new environment variable `AYON_BUILT_UNREAL_PLUGIN` to set an already existing and built Ayon Plugin for Unreal. + + +___ + +
+ + +
+Unreal: Better handling of Exceptions in UE Worker threads #5349 + +Implemented a new `UEWorker` base class to handle exception during the execution of UE Workers. + + +___ + +
+ + +
+Houdini: Add farm toggle on creation menu #5350 + +Deadline Farm publishing and Rendering for Houdini was possible with this PR #4825 farm publishing is enabled by default some ROP nodes which may surprise new users (like me).I think adding a toggle (on by default) on creation UI is better so that users will be aware that there's a farm option for this publish instance.ROPs Modified : +- [x] Mantra ROP +- [x] Karma ROP +- [x] Arnold ROP +- [x] Redshift ROP +- [x] Vray ROP + + +___ + +
+ + +
+Ftrack: Sync to avalon settings #5353 + +Added roles settings for sync to avalon action. + + +___ + +
+ + +
+Chore: Schemas inside OpenPype #5354 + +Moved/copied schemas from repository root inside openpype/pipeline. + + +___ + +
+ + +
+AYON: Addons creation enhancements #5356 + +Enhanced AYON addons creation. Fix issue with `Pattern` typehint. Zip filenames contain version. OpenPype package is skipping modules that are already separated in AYON. Updated settings of addons. + + +___ + +
+ + +
+AYON: Update staging icons #5372 + +Updated staging icons for staging mode. + + +___ + +
+ + +
+Enhancement: Houdini Update pointcache labels #5373 + +To me it's logical to find pointcaches types listed one after another, but they were named differentlySo, I made this PR to update their labels + + +___ + +
+ + +
+nuke: split write node product instance features #5389 + +Improving Write node product instances by allowing precise activation of specific features. + + +___ + +
+ + +
+Max: Use the empty modifiers in container to store AYON Parameter #5396 + +Instead of adding AYON/OP Parameter along with other attributes inside the container, empty modifiers would be created to store AYON/OP custom attributes + + +___ + +
+ + +
+AfterEffects: Removed unused imports #5397 + +Removed unused import from extract local render plugin file. + + +___ + +
+ + +
+Nuke: adding BBox knob type to settings #5405 + +Nuke knob types in settings having new `Box` type for reposition nodes like Crop or Reformat. + + +___ + +
+ + +
+SyncServer: Existence of module is optional #5413 + +Existence of SyncServer module is optional and not required. Added `sync_server` module back to ignored modules when openpype addon is created for AYON. Command `syncserver` is marked as deprecated and redirected to sync server cli. + + +___ + +
+ + +
+Webpublisher: Self contain test publish logic #5414 + +Moved test logic of publishing to webpublisher. Simplified `remote_publish` to remove webpublisher specific logic. + + +___ + +
+ + +
+Webpublisher: Cleanup targets #5418 + +Removed `remote` target from webpublisher and replaced it with 2 targets `webpublisher` and `automated`. + + +___ + +
+ + +
+nuke: update server addon settings with box #5419 + +updtaing nuke ayon server settings for Box option in knob types. + + +___ + +
+ +### **🐛 Bug fixes** + + +
+Maya: fix validate frame range on review attached to other instances #5296 + +Fixes situation where frame range validator can't be turned off on models if they are attached to reviewable camera in Maya. + + +___ + +
+ + +
+Maya: Apply project settings to creators #5303 + +Project settings were not applied to the creators. + + +___ + +
+ + +
+Maya: Validate Model Content #5336 + +`assemblies` in `cmds.ls` does not seem to work; +```python +from maya import cmds + + +content_instance = ['|group2|pSphere1_GEO', '|group2|pSphere1_GEO|pSphere1_GEOShape', '|group1|pSphere1_GEO', '|group1|pSphere1_GEO|pSphere1_GEOShape'] +assemblies = cmds.ls(content_instance, assemblies=True, long=True) +print(assemblies) +```Fixing with string splitting instead. + + +___ + +
+ + +
+Bugfix: Maya update defaults variable #5368 + +So, something was forgotten while moving out from `LegacyCreator` to `NewCreator``LegacyCreator` used `defaults` to list suggested subset names which was changed into `default_variants` in the the `NewCreator`and setting `defaults` to any values has no effect!This update affects: +- [x] Model +- [x] Set Dress + + +___ + +
+ + +
+Chore: Python 2 support fix #5375 + +Fix Python 2 support by adding `click` into python 2 dependencies and removing f-string from maya. + + +___ + +
+ + +
+Maya: do not create top level group on reference #5402 + +This PR allows to not wrapping loaded referenced assets in top level group either explicitly for artist or by configuration in Settings.Artists can control group creation in ReferenceLoader options.Default no group creation could be set by emptying `Group Name` in `project_settings/maya/load/reference_loader` + + +___ + +
+ + +
+Settings: Houdini & Maya create plugin settings #5436 + +Fixes related to Maya and Houdini settings. Renamed `defaults` to `default_variants` in plugin settings to match attribute name on create plugin in both OpenPype and AYON settings. Fixed Houdini AYON settings where were missing settings for defautlt varaints and fixed Maya AYON settings where default factory had wrong assignment. + + +___ + +
+ + +
+Maya: Hide CreateAnimation #5297 + +When converting `animation` family or loading a `rig` family, need to include the `animation` creator but hide it in creator context. + + +___ + +
+ + +
+Nuke Anamorphic slate - Read pixel aspect from input #5304 + +When asset pixel aspect differs from rendered pixel aspect, Nuke slate pixel aspect is not longer taken from asset, but is readed via ffprobe. + + +___ + +
+ + +
+Nuke - Allow ExtractReviewDataMov with no timecode knob #5305 + +ExtractReviewDataMov allows to specify file type. Trying to write some other extension than mov fails on generate_mov assuming that mov64_write_timecode knob exists. + + +___ + +
+ + +
+Nuke: removing settings schema with defaults for OpenPype #5306 + +continuation of https://github.com/ynput/OpenPype/pull/5275 + + +___ + +
+ + +
+Bugfix: Dependency without 'inputLinks' not downloaded #5337 + +Remove condition that avoids downloading dependency without `inputLinks`. + + +___ + +
+ + +
+Bugfix: Houdini Creator use selection even if it was toggled off #5359 + +When creating many product types (families) one after another without refreshing the creator window manually if you toggled `Use selection` once, all the later product types will use selection even if it was toggled offHere's Before it will keep use selection even if it was toggled off, unless you refresh window manuallyhttps://github.com/ynput/OpenPype/assets/20871534/8b890122-5b53-4c6b-897d-6a2f3aa3388aHere's After it works as expectedhttps://github.com/ynput/OpenPype/assets/20871534/6b1db990-de1b-428e-8828-04ab59a44e28 + + +___ + +
+ + +
+Houdini: Correct camera selection for karma renderer when using selected node #5360 + +When user creates the karma rop with selected camera by use selection, it will give the error message of "no render camera found in selection".This PR is to fix the bug of creating karma rop when using selected camera node in Houdini + + +___ + +
+ + +
+AYON: Environment variables and functions #5361 + +Prepare code for ayon-launcher compatibility. Fix ayon launcher subprocess calls, added more checks for `AYON_SERVER_ENABLED`, use ayon launcher suitable environment variables in AYON mode and changed outputs of some functions. Replaced usages of `OPENPYPE_REPOS_ROOT` environment variable with `PACKAGE_DIR` variable -> correct paths are used. + + +___ + +
+ + +
+Nuke: farm rendering of prerender ignore roots in nuke #5366 + +`prerender` family was using wrong subset, same as `render` which should be different. + + +___ + +
+ + +
+Bugfix: Houdini update defaults variable #5367 + +So, something was forgotten while moving out from `LegacyCreator` to `NewCreator``LegacyCreator` used `defaults` to list suggested subset names which was changed into `default_variants` in the the `NewCreator`and setting `defaults` to any values has no effect!This update affects: +- [x] Arnold ASS +- [x] Arnold ROP +- [x] Karma ROP +- [x] Mantra ROP +- [x] Redshift ROP +- [x] VRay ROP + + +___ + +
+ + +
+Publisher: Fix create/publish animation #5369 + +Use geometry movement instead of changing min/max width. + + +___ + +
+ + +
+Unreal: Move unreal splash screen to unreal #5370 + +Moved splash screen code to unreal integration and removed import from Igniter. + + +___ + +
+ + +
+Nuke: returned not cleaning of renders folder on the farm #5374 + +Previous PR enabled explicit cleanup of `renders` folder after farm publishing. This is not matching customer's workflows. Customer wants to have access to files in `renders` folder and potentially redo some frames for long frame sequences.This PR extends logic of marking rendered files for deletion only if instance doesn't have `stagingDir_persistent`.For backwards compatibility all Nuke instances have `stagingDir_persistent` set to True, eg. `renders` folder won't be cleaned after farm publish. + + +___ + +
+ + +
+Nuke: loading sequences is working #5376 + +Loading image sequences was broken after the latest release, version 3.16. However, I am pleased to inform you that it is now functioning as expected. + + +___ + +
+ + +
+AYON: Fix settings conversion for ayon addons #5377 + +AYON addon settings are available in system settings and does not have available the same values in `"modules"` subkey. + + +___ + +
+ + +
+Nuke: OCIO env var workflow #5379 + +The OCIO environment variable needs to be consistently handled across all platforms. Nuke resolves the custom OCIO config path differently depending on the platform, so we included the ocio config path in the workfile with a partial replacement using an environment variable. Additionally, for Windows sessions, we replaced backward slashes with a TCL expression. + + +___ + +
+ + +
+Unreal: Fix Unreal build script #5381 + +Define 'AYON_UNREAL_ROOT' environment variable in unreal addon. + + +___ + +
+ + +
+3dsMax: Use relative path to MAX_HOST_DIR #5382 + +Use `MAX_HOST_DIR` to calculate startup script path instead of use relative path to `OPENPYPE_ROOT` environment variable. + + +___ + +
+ + +
+Bugfix: Houdini abc validator error message #5386 + +When ABC path validator fails, it prints node objects not node paths or namesThis bug happened because of updating `get_invalid` method to return nodes instead of node pathsBeforeAfter + + +___ + +
+ + +
+Nuke: node name influence product (subset) name #5392 + +Nuke now allows users to duplicate publishing instances, making the workflow easier. By duplicating a node and changing its name, users can set the product (subset) name in the publishing context.Users now have the ability to change the variant name in Publisher, which will automatically rename the associated instance node. + + +___ + +
+ + +
+Houdini: delete redundant bgeo sop validator #5394 + +I found out that this `Validate BGEO SOP Path` validator is redundant, it catches two cases that are already implemented in "Validate Output Node". "Validate Output Node" works with `bgeo` as well as `abc` because `"pointcache"` is listed in its families + + +___ + +
+ + +
+Nuke: workfile is not reopening after change of context #5399 + +Nuke no longer reopens the latest workfile when the context is changed to a different task using the Workfile tool. The issue also affected the Script Clean (from Nuke File menu) and Close feature, but it has now been fixed. + + +___ + +
+ + +
+Bugfix: houdini hard coded project settings #5400 + +I made this PR to solve the issue with hard-coded settings in houdini + + +___ + +
+ + +
+AYON: 3dsMax settings #5401 + +Keep `adsk_3dsmax` group in applications settings. + + +___ + +
+ + +
+Bugfix: update defaults to default_variants in maya and houdini OP DCC settings #5407 + +On moving out to new creator in Maya and Houdini updating settings was missed. + + +___ + +
+ + +
+Applications: Attributes creation #5408 + +Applications addon does not cause infinite server restart loop. + + +___ + +
+ + +
+Max: fix the bug of handling Object deletion in OP Parameter #5410 + +If the object is added to the OP parameter and user delete it in the scene thereafter, it will error out the container with OP attributes. This PR resolves the bug.This PR also fixes the bug of not adding the attribute into OP parameter correctly when the user enables "use selections" to link the object into the OP parameter. + + +___ + +
+ + +
+Colorspace: including environments from launcher process #5411 + +Fixed bug in GitHub PR where the OCIO config template was not properly formatting environment variables from System Settings `general/environment`. + + +___ + +
+ + +
+Nuke: workfile template fixes #5428 + +Some bunch of small bugs needed to be fixed + + +___ + +
+ + +
+Houdini, Max: Fix missed function interface change #5430 + +This PR https://github.com/ynput/OpenPype/pull/5321/files from @kalisp missed updating the `add_render_job_env_var` in Houdini and Max as they are passing an extra arg: +``` +TypeError: add_render_job_env_var() takes 1 positional argument but 2 were given +``` + + +___ + +
+ + +
+Scene Inventory: Fix issue with 'sync_server' #5431 + +Fix accesss to `sync_server` attribute in scene inventory. + + +___ + +
+ + +
+Unpack project: Fix import issue #5433 + +Added `load_json_file`, `replace_project_documents` and `store_project_documents` to mongo init. + + +___ + +
+ + +
+Chore: Versions post fixes #5441 + +Fixed issues caused by my fault. Filled right version value to anatomy data. + + +___ + +
+ +### **📃 Testing** + + +
+Tests: Copy file_handler as it will be removed by purging ayon code #5357 + +Ayon code will get purged in the future from this repo/addon, therefore all `ayon_common` will be gone. `file_handler` gets internalized to tests as it is not used anywhere else. + + +___ + +
+ + + + ## [3.16.2](https://github.com/ynput/OpenPype/tree/3.16.2) diff --git a/openpype/version.py b/openpype/version.py index 393074c773..d7c8a71343 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.16.3-nightly.5" +__version__ = "3.16.3" diff --git a/pyproject.toml b/pyproject.toml index c4596a7edd..5e7938751e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.16.2" # OpenPype +version = "3.16.3" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From ea3f26031d7ff9cd3a6ca6d1fb4f7cca0835e79c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 11 Aug 2023 08:35:29 +0000 Subject: [PATCH 087/327] 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 5826d99d38..84f954c71b 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.16.3 - 3.16.3-nightly.5 - 3.16.3-nightly.4 - 3.16.3-nightly.3 @@ -134,7 +135,6 @@ body: - 3.14.7-nightly.7 - 3.14.7-nightly.6 - 3.14.7-nightly.5 - - 3.14.7-nightly.4 validations: required: true - type: dropdown From a9d8e57db32cbf546390f961471fbd108eea1ee1 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 11 Aug 2023 10:52:11 +0200 Subject: [PATCH 088/327] fixing changelog --- CHANGELOG.md | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6d8f01234..80d6a0d99d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -261,7 +261,7 @@ ___
Enhancement: Houdini Update pointcache labels #5373 -To me it's logical to find pointcaches types listed one after another, but they were named differentlySo, I made this PR to update their labels +To me it's logical to find pointcaches types listed one after another, but they were named differentlySo, I made this PR to update their labels ___ @@ -386,13 +386,16 @@ ___ `assemblies` in `cmds.ls` does not seem to work; ```python + from maya import cmds content_instance = ['|group2|pSphere1_GEO', '|group2|pSphere1_GEO|pSphere1_GEOShape', '|group1|pSphere1_GEO', '|group1|pSphere1_GEO|pSphere1_GEOShape'] assemblies = cmds.ls(content_instance, assemblies=True, long=True) print(assemblies) -```Fixing with string splitting instead. +``` + +Fixing with string splitting instead. ___ @@ -653,7 +656,7 @@ ___
Bugfix: Houdini abc validator error message #5386 -When ABC path validator fails, it prints node objects not node paths or namesThis bug happened because of updating `get_invalid` method to return nodes instead of node pathsBeforeAfter +When ABC path validator fails, it prints node objects not node paths or namesThis bug happened because of updating `get_invalid` method to return nodes instead of node pathsBeforeAfter ___ @@ -1189,7 +1192,7 @@ ___ Add functional base for API Documentation using Sphinx and AutoAPI. -After unsuccessful #2512, #834 and #210 this is yet another try. But this time without ambition to solve the whole issue. This is making Shinx script to work and nothing else. Any changes and improvements in API docs should be made in subsequent PRs. +After unsuccessful #2512, #834 and #210 this is yet another try. But this time without ambition to solve the whole issue. This is making Shinx script to work and nothing else. Any changes and improvements in API docs should be made in subsequent PRs. ## How to use it @@ -1200,7 +1203,7 @@ cd .\docs make.bat html ``` -or +or ```sh cd ./docs @@ -1215,7 +1218,7 @@ During the build you'll see tons of red errors that are pointing to our issues: Invalid import are usually wrong relative imports (too deep) or circular imports. 2) **Invalid doc-strings** - Doc-strings to be processed into documentation needs to follow some syntax - this can be checked by running + Doc-strings to be processed into documentation needs to follow some syntax - this can be checked by running `pydocstyle` that is already included with OpenPype 3) **Invalid markdown/rst files** md/rst files can be included inside rst files using `.. include::` directive. But they have to be properly formatted. @@ -2402,11 +2405,11 @@ ___
Houdini: Redshift ROP image format bug #5218 -Problem : -"RS_outputFileFormat" parm value was missing -and there were more "image_format" than redshift rop supports +Problem : +"RS_outputFileFormat" parm value was missing +and there were more "image_format" than redshift rop supports -Fix: +Fix: 1) removed unnecessary formats from `image_format_enum` 2) add the selected format value to `RS_outputFileFormat` ___ @@ -4583,7 +4586,7 @@ ___
Maya Load References - Add Display Handle Setting #4904 -When we load a reference in Maya using OpenPype loader, display handle is checked by default and prevent us to select easily the object in the viewport. I understand that some productions like to keep this option, so I propose to add display handle to the reference loader settings. +When we load a reference in Maya using OpenPype loader, display handle is checked by default and prevent us to select easily the object in the viewport. I understand that some productions like to keep this option, so I propose to add display handle to the reference loader settings. ___ @@ -4691,7 +4694,7 @@ ___
Patchelf version locked #4853 -For Centos dockerfile it is necessary to lock the patchelf version to the older, otherwise the build process fails. +For Centos dockerfile it is necessary to lock the patchelf version to the older, otherwise the build process fails. ___ From 899482c0af7094781a0263b4a91e1b2e1a7d65d9 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 11 Aug 2023 11:17:38 +0200 Subject: [PATCH 089/327] Add automated targets for tests (#5443) Without it plugins with 'automated' targets won't be triggered (eg `CloseAE` etc.) --- openpype/pipeline/context_tools.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/pipeline/context_tools.py b/openpype/pipeline/context_tools.py index 9ada2d42a4..f567118062 100644 --- a/openpype/pipeline/context_tools.py +++ b/openpype/pipeline/context_tools.py @@ -21,6 +21,7 @@ from openpype.client import ( from openpype.lib.events import emit_event from openpype.modules import load_modules, ModulesManager from openpype.settings import get_project_settings +from openpype.tests.lib import is_in_tests from .publish.lib import filter_pyblish_plugins from .anatomy import Anatomy @@ -142,6 +143,10 @@ def install_host(host): else: pyblish.api.register_target("local") + if is_in_tests(): + print("Registering pyblish target: automated") + pyblish.api.register_target("automated") + project_name = os.environ.get("AVALON_PROJECT") host_name = os.environ.get("AVALON_APP") From 43796c2c1c14fee0f33e8d1e2480deb0e3c19256 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 11 Aug 2023 18:23:28 +0800 Subject: [PATCH 090/327] roy's comment --- openpype/hosts/nuke/plugins/load/load_image.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/nuke/plugins/load/load_image.py b/openpype/hosts/nuke/plugins/load/load_image.py index 225365056a..0dd3a940db 100644 --- a/openpype/hosts/nuke/plugins/load/load_image.py +++ b/openpype/hosts/nuke/plugins/load/load_image.py @@ -96,7 +96,8 @@ class LoadImage(load.LoaderPlugin): file = file.replace("\\", "/") - repr_cont = context["representation"]["context"] + representation = context["representation"] + repr_cont = representation["context"] frame = repr_cont.get("frame") if frame: padding = len(frame) @@ -104,16 +105,7 @@ class LoadImage(load.LoaderPlugin): frame, format(frame_number, "0{}".format(padding))) - name_data = { - "asset": repr_cont["asset"], - "subset": repr_cont["subset"], - "representation": context["representation"]["name"], - "ext": repr_cont["representation"], - "id": context["representation"]["_id"], - "class_name": self.__class__.__name__ - } - - read_name = self.node_name_template.format(**name_data) + read_name = self._get_node_name(representation) # Create the Loader with the filename path set with viewer_update_and_undo_stop(): From fdc8ccd4194dbe1d8a79233d96fa9fdd4aa685d7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 11 Aug 2023 14:14:56 +0200 Subject: [PATCH 091/327] farm: asymmetric handles fixed --- openpype/pipeline/farm/pyblish_functions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/farm/pyblish_functions.py b/openpype/pipeline/farm/pyblish_functions.py index 9278b0efc5..8b9058359e 100644 --- a/openpype/pipeline/farm/pyblish_functions.py +++ b/openpype/pipeline/farm/pyblish_functions.py @@ -116,8 +116,8 @@ def get_time_data_from_instance_or_context(instance): instance.context.data.get("fps")), handle_start=(instance.data.get("handleStart") or instance.context.data.get("handleStart")), # noqa: E501 - handle_end=(instance.data.get("handleStart") or - instance.context.data.get("handleStart")) + handle_end=(instance.data.get("handleEnd") or + instance.context.data.get("handleEnd")) ) From 80114b24fa6571087a979906f8a8a83337bf8182 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 11 Aug 2023 14:50:05 +0200 Subject: [PATCH 092/327] TVPaint: Fix 'repeat' behavior (#5412) * adde frame start to repreat frame matching * removed "loop" from behaviors --- openpype/hosts/tvpaint/api/lib.py | 4 ++-- openpype/hosts/tvpaint/lib.py | 10 ++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/tvpaint/api/lib.py b/openpype/hosts/tvpaint/api/lib.py index 49846d7f29..f8b8c29cdb 100644 --- a/openpype/hosts/tvpaint/api/lib.py +++ b/openpype/hosts/tvpaint/api/lib.py @@ -233,7 +233,7 @@ def get_layers_pre_post_behavior(layer_ids, communicator=None): Pre and Post behaviors is enumerator of possible values: - "none" - - "repeat" / "loop" + - "repeat" - "pingpong" - "hold" @@ -242,7 +242,7 @@ def get_layers_pre_post_behavior(layer_ids, communicator=None): { 0: { "pre": "none", - "post": "loop" + "post": "repeat" } } ``` diff --git a/openpype/hosts/tvpaint/lib.py b/openpype/hosts/tvpaint/lib.py index 95653b6ecb..97cf8d3633 100644 --- a/openpype/hosts/tvpaint/lib.py +++ b/openpype/hosts/tvpaint/lib.py @@ -77,13 +77,15 @@ def _calculate_pre_behavior_copy( for frame_idx in range(range_start, layer_frame_start): output_idx_by_frame_idx[frame_idx] = first_exposure_frame - elif pre_beh in ("loop", "repeat"): + elif pre_beh == "repeat": # Loop backwards from last frame of layer for frame_idx in reversed(range(range_start, layer_frame_start)): eq_frame_idx_offset = ( (layer_frame_end - frame_idx) % frame_count ) - eq_frame_idx = layer_frame_end - eq_frame_idx_offset + eq_frame_idx = layer_frame_start + ( + layer_frame_end - eq_frame_idx_offset + ) output_idx_by_frame_idx[frame_idx] = eq_frame_idx elif pre_beh == "pingpong": @@ -139,10 +141,10 @@ def _calculate_post_behavior_copy( for frame_idx in range(layer_frame_end + 1, range_end + 1): output_idx_by_frame_idx[frame_idx] = last_exposure_frame - elif post_beh in ("loop", "repeat"): + elif post_beh == "repeat": # Loop backwards from last frame of layer for frame_idx in range(layer_frame_end + 1, range_end + 1): - eq_frame_idx = frame_idx % frame_count + eq_frame_idx = layer_frame_start + (frame_idx % frame_count) output_idx_by_frame_idx[frame_idx] = eq_frame_idx elif post_beh == "pingpong": From a2a35e8252edf94c7610e84130703e0a7d3f4b4f Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 11 Aug 2023 14:01:37 +0100 Subject: [PATCH 093/327] General: Navigation to Folder from Launcher (#5404) * Basic implementation of navigation to folder from launcher * Allow the action to appear without a task selected * Added multiplatform support * Improved code to open file browser in different platforms Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * Fixed missing import * Improved implementation to get path Co-authored-by: Roy Nieterau * Hound fixes * Use qtpy instead of Qt * Changed icon and label * Fix navigation not navigating to task folder * Implemented suggestions Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * Add comment for clarity * change behavior to strictly use task of asset path without finding first available path * require asset name * raise exceptions to show a message to user --------- Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Co-authored-by: Roy Nieterau Co-authored-by: Jakub Trllo --- .../plugins/actions/open_file_explorer.py | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 openpype/plugins/actions/open_file_explorer.py diff --git a/openpype/plugins/actions/open_file_explorer.py b/openpype/plugins/actions/open_file_explorer.py new file mode 100644 index 0000000000..e4fbd91143 --- /dev/null +++ b/openpype/plugins/actions/open_file_explorer.py @@ -0,0 +1,125 @@ +import os +import platform +import subprocess + +from string import Formatter +from openpype.client import ( + get_project, + get_asset_by_name, +) +from openpype.pipeline import ( + Anatomy, + LauncherAction, +) +from openpype.pipeline.template_data import get_template_data + + +class OpenTaskPath(LauncherAction): + name = "open_task_path" + label = "Explore here" + icon = "folder-open" + order = 500 + + def is_compatible(self, session): + """Return whether the action is compatible with the session""" + return bool(session.get("AVALON_ASSET")) + + def process(self, session, **kwargs): + from qtpy import QtCore, QtWidgets + + project_name = session["AVALON_PROJECT"] + asset_name = session["AVALON_ASSET"] + task_name = session.get("AVALON_TASK", None) + + path = self._get_workdir(project_name, asset_name, task_name) + if not path: + return + + app = QtWidgets.QApplication.instance() + ctrl_pressed = QtCore.Qt.ControlModifier & app.keyboardModifiers() + if ctrl_pressed: + # Copy path to clipboard + self.copy_path_to_clipboard(path) + else: + self.open_in_explorer(path) + + def _find_first_filled_path(self, path): + if not path: + return "" + + fields = set() + for item in Formatter().parse(path): + _, field_name, format_spec, conversion = item + if not field_name: + continue + conversion = "!{}".format(conversion) if conversion else "" + format_spec = ":{}".format(format_spec) if format_spec else "" + orig_key = "{{{}{}{}}}".format( + field_name, conversion, format_spec) + fields.add(orig_key) + + for field in fields: + path = path.split(field, 1)[0] + return path + + def _get_workdir(self, project_name, asset_name, task_name): + project = get_project(project_name) + asset = get_asset_by_name(project_name, asset_name) + + data = get_template_data(project, asset, task_name) + + anatomy = Anatomy(project_name) + workdir = anatomy.templates_obj["work"]["folder"].format(data) + + # Remove any potential un-formatted parts of the path + valid_workdir = self._find_first_filled_path(workdir) + + # Path is not filled at all + if not valid_workdir: + raise AssertionError("Failed to calculate workdir.") + + # Normalize + valid_workdir = os.path.normpath(valid_workdir) + if os.path.exists(valid_workdir): + return valid_workdir + + # If task was selected, try to find asset path only to asset + if not task_name: + raise AssertionError("Folder does not exist.") + + data.pop("task", None) + workdir = anatomy.templates_obj["work"]["folder"].format(data) + valid_workdir = self._find_first_filled_path(workdir) + if valid_workdir: + # Normalize + valid_workdir = os.path.normpath(valid_workdir) + if os.path.exists(valid_workdir): + return valid_workdir + raise AssertionError("Folder does not exist.") + + @staticmethod + def open_in_explorer(path): + platform_name = platform.system().lower() + if platform_name == "windows": + args = ["start", path] + elif platform_name == "darwin": + args = ["open", "-na", path] + elif platform_name == "linux": + args = ["xdg-open", path] + else: + raise RuntimeError(f"Unknown platform {platform.system()}") + # Make sure path is converted correctly for 'os.system' + os.system(subprocess.list2cmdline(args)) + + @staticmethod + def copy_path_to_clipboard(path): + from qtpy import QtWidgets + + path = path.replace("\\", "/") + print(f"Copied to clipboard: {path}") + app = QtWidgets.QApplication.instance() + assert app, "Must have running QApplication instance" + + # Set to Clipboard + clipboard = QtWidgets.QApplication.clipboard() + clipboard.setText(os.path.normpath(path)) From 8b128d91bcff2570712ff442c9ea35feecb09c84 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 11 Aug 2023 16:12:05 +0200 Subject: [PATCH 094/327] Maya: allow not creation of group for Import loaders (#5427) * OP-6357 - removed unneeded import * OP-6357 - extracted logic for getting custom group and namespace from Settings Mimicing logic in ReferenceLoader, eg. group could be left empty >> no groupping of imported subset. * OP-6357 - same logic for abc animation as Reference * OP-6357 - same logic for yeti rig as ReferenceLoder Allows to not create wrapping group. * OP-6357 - added separate import_loader to settings Could be used to not creating wrapping groups when Group kept empty. * OP-6357 - added product subset conversion for ayon settings * OP-6357 - fix using correct variable Artist input comes from `data` not directly from self.options * OP-6357 - add attach_to_root to options to allow control by same key * OP-6357 - added docstring * Added settings for Import loaders in maya * OP-6357 - refactored formatting --- openpype/hosts/maya/api/plugin.py | 86 ++++++++++++------- .../maya/plugins/load/_load_animation.py | 11 ++- openpype/hosts/maya/plugins/load/actions.py | 23 ++--- .../hosts/maya/plugins/load/load_reference.py | 3 +- .../hosts/maya/plugins/load/load_yeti_rig.py | 11 ++- openpype/settings/ayon_settings.py | 7 ++ .../defaults/project_settings/maya.json | 4 + .../schemas/schema_maya_load.json | 22 +++++ server_addon/maya/server/settings/loaders.py | 9 ++ server_addon/maya/server/version.py | 2 +- 10 files changed, 128 insertions(+), 50 deletions(-) diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index 4d467840dd..f705133e4f 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -523,6 +523,55 @@ class RenderlayerCreator(NewCreator, MayaCreatorBase): class Loader(LoaderPlugin): hosts = ["maya"] + def get_custom_namespace_and_group(self, context, options, loader_key): + """Queries Settings to get custom template for namespace and group. + + Group template might be empty >> this forces to not wrap imported items + into separate group. + + Args: + context (dict) + options (dict): artist modifiable options from dialog + loader_key (str): key to get separate configuration from Settings + ('reference_loader'|'import_loader') + """ + options["attach_to_root"] = True + + asset = context['asset'] + subset = context['subset'] + settings = get_project_settings(context['project']['name']) + custom_naming = settings['maya']['load'][loader_key] + + if not custom_naming['namespace']: + raise LoadError("No namespace specified in " + "Maya ReferenceLoader settings") + elif not custom_naming['group_name']: + self.log.debug("No custom group_name, no group will be created.") + options["attach_to_root"] = False + + formatting_data = { + "asset_name": asset['name'], + "asset_type": asset['type'], + "folder": { + "name": asset["name"], + }, + "subset": subset['name'], + "family": ( + subset['data'].get('family') or + subset['data']['families'][0] + ) + } + + custom_namespace = custom_naming['namespace'].format( + **formatting_data + ) + + custom_group_name = custom_naming['group_name'].format( + **formatting_data + ) + + return custom_group_name, custom_namespace, options + class ReferenceLoader(Loader): """A basic ReferenceLoader for Maya @@ -565,42 +614,13 @@ class ReferenceLoader(Loader): path = self.filepath_from_context(context) assert os.path.exists(path), "%s does not exist." % path - asset = context['asset'] - subset = context['subset'] - settings = get_project_settings(context['project']['name']) - custom_naming = settings['maya']['load']['reference_loader'] - loaded_containers = [] - - if not custom_naming['namespace']: - raise LoadError("No namespace specified in " - "Maya ReferenceLoader settings") - elif not custom_naming['group_name']: - self.log.debug("No custom group_name, no group will be created.") - options["attach_to_root"] = False - - formatting_data = { - "asset_name": asset['name'], - "asset_type": asset['type'], - "folder": { - "name": asset["name"], - }, - "subset": subset['name'], - "family": ( - subset['data'].get('family') or - subset['data']['families'][0] - ) - } - - custom_namespace = custom_naming['namespace'].format( - **formatting_data - ) - - custom_group_name = custom_naming['group_name'].format( - **formatting_data - ) + custom_group_name, custom_namespace, options = \ + self.get_custom_namespace_and_group(context, options, + "reference_loader") count = options.get("count") or 1 + loaded_containers = [] for c in range(0, count): namespace = lib.get_custom_namespace(custom_namespace) group_name = "{}:{}".format( diff --git a/openpype/hosts/maya/plugins/load/_load_animation.py b/openpype/hosts/maya/plugins/load/_load_animation.py index 49792b2806..981b9ef434 100644 --- a/openpype/hosts/maya/plugins/load/_load_animation.py +++ b/openpype/hosts/maya/plugins/load/_load_animation.py @@ -33,6 +33,13 @@ class AbcLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): suffix="_abc" ) + attach_to_root = options.get("attach_to_root", True) + group_name = options["group_name"] + + # no group shall be created + if not attach_to_root: + group_name = namespace + # hero_001 (abc) # asset_counter{optional} path = self.filepath_from_context(context) @@ -41,8 +48,8 @@ class AbcLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): nodes = cmds.file(file_url, namespace=namespace, sharedReferenceFile=False, - groupReference=True, - groupName=options['group_name'], + groupReference=attach_to_root, + groupName=group_name, reference=True, returnNewNodes=True) diff --git a/openpype/hosts/maya/plugins/load/actions.py b/openpype/hosts/maya/plugins/load/actions.py index 348657e592..d347ef0d08 100644 --- a/openpype/hosts/maya/plugins/load/actions.py +++ b/openpype/hosts/maya/plugins/load/actions.py @@ -5,8 +5,9 @@ import qargparse from openpype.pipeline import load from openpype.hosts.maya.api.lib import ( maintained_selection, - unique_namespace + get_custom_namespace ) +import openpype.hosts.maya.api.plugin class SetFrameRangeLoader(load.LoaderPlugin): @@ -83,7 +84,7 @@ class SetFrameRangeWithHandlesLoader(load.LoaderPlugin): animationEndTime=end) -class ImportMayaLoader(load.LoaderPlugin): +class ImportMayaLoader(openpype.hosts.maya.api.plugin.Loader): """Import action for Maya (unmanaged) Warning: @@ -130,13 +131,14 @@ class ImportMayaLoader(load.LoaderPlugin): if choice is False: return - asset = context['asset'] + custom_group_name, custom_namespace, options = \ + self.get_custom_namespace_and_group(context, data, + "import_loader") - namespace = namespace or unique_namespace( - asset["name"] + "_", - prefix="_" if asset["name"][0].isdigit() else "", - suffix="_", - ) + namespace = get_custom_namespace(custom_namespace) + + if not options.get("attach_to_root", True): + custom_group_name = namespace path = self.filepath_from_context(context) with maintained_selection(): @@ -145,8 +147,9 @@ class ImportMayaLoader(load.LoaderPlugin): preserveReferences=True, namespace=namespace, returnNewNodes=True, - groupReference=True, - groupName="{}:{}".format(namespace, name)) + groupReference=options.get("attach_to_root", + True), + groupName=custom_group_name) if data.get("clean_import", False): remove_attributes = ["cbId"] diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index c8d3b3128a..91767249e0 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -9,8 +9,7 @@ from openpype.hosts.maya.api.lib import ( maintained_selection, get_container_members, parent_nodes, - create_rig_animation_instance, - get_reference_node + create_rig_animation_instance ) diff --git a/openpype/hosts/maya/plugins/load/load_yeti_rig.py b/openpype/hosts/maya/plugins/load/load_yeti_rig.py index c9dfe9478b..6cfcffe27d 100644 --- a/openpype/hosts/maya/plugins/load/load_yeti_rig.py +++ b/openpype/hosts/maya/plugins/load/load_yeti_rig.py @@ -19,8 +19,15 @@ class YetiRigLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): def process_reference( self, context, name=None, namespace=None, options=None ): - group_name = options['group_name'] path = self.filepath_from_context(context) + + attach_to_root = options.get("attach_to_root", True) + group_name = options["group_name"] + + # no group shall be created + if not attach_to_root: + group_name = namespace + with lib.maintained_selection(): file_url = self.prepare_root_value( path, context["project"]["name"] @@ -30,7 +37,7 @@ class YetiRigLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): namespace=namespace, reference=True, returnNewNodes=True, - groupReference=True, + groupReference=attach_to_root, groupName=group_name ) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index 78eed359a3..6237756943 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -602,6 +602,13 @@ def _convert_maya_project_settings(ayon_settings, output): .replace("{product[name]}", "{subset}") ) + if ayon_maya_load.get("import_loader"): + import_loader = ayon_maya_load["import_loader"] + import_loader["namespace"] = ( + import_loader["namespace"] + .replace("{product[name]}", "{subset}") + ) + output["maya"] = ayon_maya diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index e1c6d2d827..d2fb7b0864 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -1463,6 +1463,10 @@ "namespace": "{asset_name}_{subset}_##_", "group_name": "_GRP", "display_handle": true + }, + "import_loader": { + "namespace": "{asset_name}_{subset}_##_", + "group_name": "_GRP" } }, "workfile_build": { diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_load.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_load.json index 4b6b97ab4e..e73d39c06d 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_load.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_load.json @@ -121,6 +121,28 @@ "label": "Display Handle On Load References" } ] + }, + { + "type": "dict", + "collapsible": true, + "key": "import_loader", + "label": "Import Loader", + "children": [ + { + "type": "text", + "label": "Namespace", + "key": "namespace" + }, + { + "type": "text", + "label": "Group name", + "key": "group_name" + }, + { + "type": "label", + "label": "Here's a link to the doc where you can find explanations about customing the naming of referenced assets: https://openpype.io/docs/admin_hosts_maya#load-plugins" + } + ] } ] } diff --git a/server_addon/maya/server/settings/loaders.py b/server_addon/maya/server/settings/loaders.py index 60fc2a1cdd..29966bb6dd 100644 --- a/server_addon/maya/server/settings/loaders.py +++ b/server_addon/maya/server/settings/loaders.py @@ -45,6 +45,11 @@ class ReferenceLoaderModel(BaseSettingsModel): display_handle: bool = Field(title="Display Handle On Load References") +class ImportLoaderModel(BaseSettingsModel): + namespace: str = Field(title="Namespace") + group_name: str = Field(title="Group name") + + class LoadersModel(BaseSettingsModel): colors: ColorsSetting = Field( default_factory=ColorsSetting, @@ -55,6 +60,10 @@ class LoadersModel(BaseSettingsModel): title="Reference Loader" ) + import_loader: ImportLoaderModel = Field( + default_factory=ImportLoaderModel, + title="Import Loader" + ) DEFAULT_LOADERS_SETTING = { "colors": { diff --git a/server_addon/maya/server/version.py b/server_addon/maya/server/version.py index df0c92f1e2..e57ad00718 100644 --- a/server_addon/maya/server/version.py +++ b/server_addon/maya/server/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring addon version.""" -__version__ = "0.1.2" +__version__ = "0.1.3" From eaf248fefedca34052445f7e1ed18aa44a6ed35d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 11 Aug 2023 16:22:07 +0200 Subject: [PATCH 095/327] AYON: Thumbnails cache and api prep (#5437) * moved thumbnails cache from ayon api to server codebase * use cache in AYON thumbnail resolver and prepare it for new api methods --- openpype/client/server/thumbnails.py | 229 +++++++++++++++++++++++++++ openpype/pipeline/thumbnail.py | 49 ++++-- 2 files changed, 269 insertions(+), 9 deletions(-) create mode 100644 openpype/client/server/thumbnails.py diff --git a/openpype/client/server/thumbnails.py b/openpype/client/server/thumbnails.py new file mode 100644 index 0000000000..dc649b9651 --- /dev/null +++ b/openpype/client/server/thumbnails.py @@ -0,0 +1,229 @@ +"""Cache of thumbnails downloaded from AYON server. + +Thumbnails are cached to appdirs to predefined directory. + +This should be moved to thumbnails logic in pipeline but because it would +overflow OpenPype logic it's here for now. +""" + +import os +import time +import collections + +import appdirs + +FileInfo = collections.namedtuple( + "FileInfo", + ("path", "size", "modification_time") +) + + +class AYONThumbnailCache: + """Cache of thumbnails on local storage. + + Thumbnails are cached to appdirs to predefined directory. Each project has + own subfolder with thumbnails -> that's because each project has own + thumbnail id validation and file names are thumbnail ids with matching + extension. Extensions are predefined (.png and .jpeg). + + Cache has cleanup mechanism which is triggered on initialized by default. + + The cleanup has 2 levels: + 1. soft cleanup which remove all files that are older then 'days_alive' + 2. max size cleanup which remove all files until the thumbnails folder + contains less then 'max_filesize' + - this is time consuming so it's not triggered automatically + + Args: + cleanup (bool): Trigger soft cleanup (Cleanup expired thumbnails). + """ + + # Lifetime of thumbnails (in seconds) + # - default 3 days + days_alive = 3 + # Max size of thumbnail directory (in bytes) + # - default 2 Gb + max_filesize = 2 * 1024 * 1024 * 1024 + + def __init__(self, cleanup=True): + self._thumbnails_dir = None + self._days_alive_secs = self.days_alive * 24 * 60 * 60 + if cleanup: + self.cleanup() + + def get_thumbnails_dir(self): + """Root directory where thumbnails are stored. + + Returns: + str: Path to thumbnails root. + """ + + if self._thumbnails_dir is None: + # TODO use generic function + directory = appdirs.user_data_dir("AYON", "Ynput") + self._thumbnails_dir = os.path.join(directory, "thumbnails") + return self._thumbnails_dir + + thumbnails_dir = property(get_thumbnails_dir) + + def get_thumbnails_dir_file_info(self): + """Get information about all files in thumbnails directory. + + Returns: + List[FileInfo]: List of file information about all files. + """ + + thumbnails_dir = self.thumbnails_dir + files_info = [] + if not os.path.exists(thumbnails_dir): + return files_info + + for root, _, filenames in os.walk(thumbnails_dir): + for filename in filenames: + path = os.path.join(root, filename) + files_info.append(FileInfo( + path, os.path.getsize(path), os.path.getmtime(path) + )) + return files_info + + def get_thumbnails_dir_size(self, files_info=None): + """Got full size of thumbnail directory. + + Args: + files_info (List[FileInfo]): Prepared file information about + files in thumbnail directory. + + Returns: + int: File size of all files in thumbnail directory. + """ + + if files_info is None: + files_info = self.get_thumbnails_dir_file_info() + + if not files_info: + return 0 + + return sum( + file_info.size + for file_info in files_info + ) + + def cleanup(self, check_max_size=False): + """Cleanup thumbnails directory. + + Args: + check_max_size (bool): Also cleanup files to match max size of + thumbnails directory. + """ + + thumbnails_dir = self.get_thumbnails_dir() + # Skip if thumbnails dir does not exists yet + if not os.path.exists(thumbnails_dir): + return + + self._soft_cleanup(thumbnails_dir) + if check_max_size: + self._max_size_cleanup(thumbnails_dir) + + def _soft_cleanup(self, thumbnails_dir): + current_time = time.time() + for root, _, filenames in os.walk(thumbnails_dir): + for filename in filenames: + path = os.path.join(root, filename) + modification_time = os.path.getmtime(path) + if current_time - modification_time > self._days_alive_secs: + os.remove(path) + + def _max_size_cleanup(self, thumbnails_dir): + files_info = self.get_thumbnails_dir_file_info() + size = self.get_thumbnails_dir_size(files_info) + if size < self.max_filesize: + return + + sorted_file_info = collections.deque( + sorted(files_info, key=lambda item: item.modification_time) + ) + diff = size - self.max_filesize + while diff > 0: + if not sorted_file_info: + break + + file_info = sorted_file_info.popleft() + diff -= file_info.size + os.remove(file_info.path) + + def get_thumbnail_filepath(self, project_name, thumbnail_id): + """Get thumbnail by thumbnail id. + + Args: + project_name (str): Name of project. + thumbnail_id (str): Thumbnail id. + + Returns: + Union[str, None]: Path to thumbnail image or None if thumbnail + is not cached yet. + """ + + if not thumbnail_id: + return None + + for ext in ( + ".png", + ".jpeg", + ): + filepath = os.path.join( + self.thumbnails_dir, project_name, thumbnail_id + ext + ) + if os.path.exists(filepath): + return filepath + return None + + def get_project_dir(self, project_name): + """Path to root directory for specific project. + + Args: + project_name (str): Name of project for which root directory path + should be returned. + + Returns: + str: Path to root of project's thumbnails. + """ + + return os.path.join(self.thumbnails_dir, project_name) + + def make_sure_project_dir_exists(self, project_name): + project_dir = self.get_project_dir(project_name) + if not os.path.exists(project_dir): + os.makedirs(project_dir) + return project_dir + + def store_thumbnail(self, project_name, thumbnail_id, content, mime_type): + """Store thumbnail to cache folder. + + Args: + project_name (str): Project where the thumbnail belong to. + thumbnail_id (str): Id of thumbnail. + content (bytes): Byte content of thumbnail file. + mime_data (str): Type of content. + + Returns: + str: Path to cached thumbnail image file. + """ + + if mime_type == "image/png": + ext = ".png" + elif mime_type == "image/jpeg": + ext = ".jpeg" + else: + raise ValueError( + "Unknown mime type for thumbnail \"{}\"".format(mime_type)) + + project_dir = self.make_sure_project_dir_exists(project_name) + thumbnail_path = os.path.join(project_dir, thumbnail_id + ext) + with open(thumbnail_path, "wb") as stream: + stream.write(content) + + current_time = time.time() + os.utime(thumbnail_path, (current_time, current_time)) + + return thumbnail_path diff --git a/openpype/pipeline/thumbnail.py b/openpype/pipeline/thumbnail.py index 9d4a6f3e48..b2b3679450 100644 --- a/openpype/pipeline/thumbnail.py +++ b/openpype/pipeline/thumbnail.py @@ -3,6 +3,7 @@ import copy import logging from openpype import AYON_SERVER_ENABLED +from openpype.lib import Logger from openpype.client import get_project from . import legacy_io from .anatomy import Anatomy @@ -11,13 +12,13 @@ from .plugin_discover import ( register_plugin, register_plugin_path, ) -log = logging.getLogger(__name__) def get_thumbnail_binary(thumbnail_entity, thumbnail_type, dbcon=None): if not thumbnail_entity: return + log = Logger.get_logger(__name__) resolvers = discover_thumbnail_resolvers() resolvers = sorted(resolvers, key=lambda cls: cls.priority) if dbcon is None: @@ -133,6 +134,16 @@ class BinaryThumbnail(ThumbnailResolver): class ServerThumbnailResolver(ThumbnailResolver): + _cache = None + + @classmethod + def _get_cache(cls): + if cls._cache is None: + from openpype.client.server.thumbnails import AYONThumbnailCache + + cls._cache = AYONThumbnailCache() + return cls._cache + def process(self, thumbnail_entity, thumbnail_type): if not AYON_SERVER_ENABLED: return None @@ -142,20 +153,40 @@ class ServerThumbnailResolver(ThumbnailResolver): if not entity_type or not entity_id: return None - from openpype.client.server.server_api import get_server_api_connection + import ayon_api project_name = self.dbcon.active_project() thumbnail_id = thumbnail_entity["_id"] - con = get_server_api_connection() - filepath = con.get_thumbnail( - project_name, entity_type, entity_id, thumbnail_id - ) - content = None + + cache = self._get_cache() + filepath = cache.get_thumbnail_filepath(project_name, thumbnail_id) if filepath: with open(filepath, "rb") as stream: - content = stream.read() + return stream.read() - return content + # This is new way how thumbnails can be received from server + # - output is 'ThumbnailContent' object + if hasattr(ayon_api, "get_thumbnail_by_id"): + result = ayon_api.get_thumbnail_by_id(thumbnail_id) + if result.is_valid: + filepath = cache.store_thumbnail( + project_name, + thumbnail_id, + result.content, + result.content_type + ) + else: + # Backwards compatibility for ayon api where 'get_thumbnail_by_id' + # is not implemented and output is filepath + filepath = ayon_api.get_thumbnail( + project_name, entity_type, entity_id, thumbnail_id + ) + + if not filepath: + return None + + with open(filepath, "rb") as stream: + return stream.read() # Thumbnail resolvers From dd27f4e839abe491d54b0183b58dc0dbbe16dc6a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 11 Aug 2023 16:23:27 +0200 Subject: [PATCH 096/327] AYON: Apply unknown ayon settings first (#5435) * apply unknown ayon settings first * added "Main" to empty default variants * use 'default_variants' in aftereffects creator --- .../hosts/aftereffects/plugins/create/create_render.py | 5 ++++- openpype/settings/ayon_settings.py | 8 ++++++++ .../settings/defaults/project_settings/aftereffects.json | 2 +- openpype/settings/defaults/project_settings/maya.json | 4 +++- .../projects_schema/schema_project_aftereffects.json | 2 +- 5 files changed, 17 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/create/create_render.py b/openpype/hosts/aftereffects/plugins/create/create_render.py index fa79fac78f..dcf424b44f 100644 --- a/openpype/hosts/aftereffects/plugins/create/create_render.py +++ b/openpype/hosts/aftereffects/plugins/create/create_render.py @@ -28,7 +28,6 @@ class RenderCreator(Creator): create_allow_context_change = True # Settings - default_variants = [] mark_for_review = True def create(self, subset_name_from_ui, data, pre_create_data): @@ -171,6 +170,10 @@ class RenderCreator(Creator): ) self.mark_for_review = plugin_settings["mark_for_review"] + self.default_variants = plugin_settings.get( + "default_variants", + plugin_settings.get("defaults") or [] + ) def get_detail_description(self): return """Creator for Render instances diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index 6237756943..50abfe4839 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -301,6 +301,10 @@ def convert_system_settings(ayon_settings, default_settings, addon_versions): if "core" in ayon_settings: _convert_general(ayon_settings, output, default_settings) + for key, value in ayon_settings.items(): + if key not in output: + output[key] = value + for key, value in default_settings.items(): if key not in output: output[key] = value @@ -1272,6 +1276,10 @@ def convert_project_settings(ayon_settings, default_settings): _convert_global_project_settings(ayon_settings, output, default_settings) + for key, value in ayon_settings.items(): + if key not in output: + output[key] = value + for key, value in default_settings.items(): if key not in output: output[key] = value diff --git a/openpype/settings/defaults/project_settings/aftereffects.json b/openpype/settings/defaults/project_settings/aftereffects.json index 63f544e536..77ccb74410 100644 --- a/openpype/settings/defaults/project_settings/aftereffects.json +++ b/openpype/settings/defaults/project_settings/aftereffects.json @@ -12,7 +12,7 @@ }, "create": { "RenderCreator": { - "defaults": [ + "default_variants": [ "Main" ], "mark_for_review": true diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index d2fb7b0864..38f14ec022 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -547,7 +547,9 @@ }, "CreateUnrealSkeletalMesh": { "enabled": true, - "default_variants": [], + "default_variants": [ + "Main" + ], "joint_hints": "jnt_org" }, "CreateMultiverseLook": { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json b/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json index 35b8fede86..72f09a641d 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json @@ -32,7 +32,7 @@ "children": [ { "type": "list", - "key": "defaults", + "key": "default_variants", "label": "Default Variants", "object_type": "text", "docstring": "Fill default variant(s) (like 'Main' or 'Default') used in subset name creation." From f5314db3ad8c765b44b578cbfba5f1959c1f91a5 Mon Sep 17 00:00:00 2001 From: FadyFS <135602303+FadyFS@users.noreply.github.com> Date: Fri, 11 Aug 2023 16:37:53 +0200 Subject: [PATCH 097/327] site config added (#5220) --- openpype/tools/settings/local_settings/projects_widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/settings/local_settings/projects_widget.py b/openpype/tools/settings/local_settings/projects_widget.py index 68e144f87b..f2b6535115 100644 --- a/openpype/tools/settings/local_settings/projects_widget.py +++ b/openpype/tools/settings/local_settings/projects_widget.py @@ -286,7 +286,7 @@ class SitesWidget(QtWidgets.QWidget): continue site_inputs = [] - site_config = site_configs[site_name] + site_config = site_configs.get(site_name, {}) for root_name, path_entity in site_config.get("root", {}).items(): if not path_entity: continue From fc5e52e9ab5f81e07b126e4b195461a1d5a3f44d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Fri, 11 Aug 2023 16:47:14 +0200 Subject: [PATCH 098/327] Feature: Download last published workfile specify version (#4998) Co-authored-by: Petr Kalis --- .../pre_copy_last_published_workfile.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/openpype/modules/sync_server/launch_hooks/pre_copy_last_published_workfile.py b/openpype/modules/sync_server/launch_hooks/pre_copy_last_published_workfile.py index 77f6933756..047e35e3ac 100644 --- a/openpype/modules/sync_server/launch_hooks/pre_copy_last_published_workfile.py +++ b/openpype/modules/sync_server/launch_hooks/pre_copy_last_published_workfile.py @@ -116,6 +116,18 @@ class CopyLastPublishedWorkfile(PreLaunchHook): "task": {"name": task_name, "type": task_type} } + # Add version filter + workfile_version = self.launch_context.data.get("workfile_version", -1) + if workfile_version > 0 and workfile_version not in {None, "last"}: + context_filters["version"] = self.launch_context.data[ + "workfile_version" + ] + + # Only one version will be matched + version_index = 0 + else: + version_index = workfile_version + workfile_representations = list(get_representations( project_name, context_filters=context_filters @@ -133,9 +145,10 @@ class CopyLastPublishedWorkfile(PreLaunchHook): lambda r: r["context"].get("version") is not None, workfile_representations ) - workfile_representation = max( + # Get workfile version + workfile_representation = sorted( filtered_repres, key=lambda r: r["context"]["version"] - ) + )[version_index] # Copy file and substitute path last_published_workfile_path = download_last_published_workfile( From d0ac9c1f2ec267ee83a31f07b787a2c98051d894 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 11 Aug 2023 18:12:29 +0200 Subject: [PATCH 099/327] Added missing defaults for import_loader (#5447) Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- server_addon/maya/server/settings/loaders.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server_addon/maya/server/settings/loaders.py b/server_addon/maya/server/settings/loaders.py index 29966bb6dd..ed6b6fd2ac 100644 --- a/server_addon/maya/server/settings/loaders.py +++ b/server_addon/maya/server/settings/loaders.py @@ -120,5 +120,10 @@ DEFAULT_LOADERS_SETTING = { "namespace": "{folder[name]}_{product[name]}_##_", "group_name": "_GRP", "display_handle": True + }, + "import_loader": { + "namespace": "{folder[name]}_{product[name]}_##_", + "group_name": "_GRP", + "display_handle": True } } From f9babce983a78a57ef0d58dff68a381d8339ad61 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 12 Aug 2023 03:24:25 +0000 Subject: [PATCH 100/327] [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 d7c8a71343..afbac53385 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.16.3" +__version__ = "3.16.4-nightly.1" From 949b6ae33866c19e744039a8fde0092ca7174a61 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 12 Aug 2023 03:25:08 +0000 Subject: [PATCH 101/327] 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 84f954c71b..96fcc38d13 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.16.4-nightly.1 - 3.16.3 - 3.16.3-nightly.5 - 3.16.3-nightly.4 @@ -134,7 +135,6 @@ body: - 3.14.7-nightly.8 - 3.14.7-nightly.7 - 3.14.7-nightly.6 - - 3.14.7-nightly.5 validations: required: true - type: dropdown From 67840465ab7cdd2c9f282b13324d53620cbce60d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 14 Aug 2023 14:36:35 +0800 Subject: [PATCH 102/327] update the attribute after OP Param update --- openpype/hosts/max/api/pipeline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/api/pipeline.py b/openpype/hosts/max/api/pipeline.py index 82470dd510..36c29ddbbb 100644 --- a/openpype/hosts/max/api/pipeline.py +++ b/openpype/hosts/max/api/pipeline.py @@ -194,4 +194,4 @@ def load_OpenpypeData(container, loaded_nodes): # Setting the property rt.setProperty( - container.openPypeData, "all_handles", node_list) + container.modifiers[0].openPypeData, "all_handles", node_list) From cb086d113ec10131663d17d3a9e06495558f840d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 14 Aug 2023 18:06:23 +0800 Subject: [PATCH 103/327] clean up the load OpenpypeData code --- openpype/hosts/max/api/pipeline.py | 25 ++++--------------- .../hosts/max/plugins/load/load_camera_fbx.py | 4 +-- openpype/hosts/max/plugins/load/load_model.py | 11 +++----- .../hosts/max/plugins/load/load_model_fbx.py | 4 +-- .../hosts/max/plugins/load/load_model_obj.py | 4 +-- .../hosts/max/plugins/load/load_model_usd.py | 6 ++--- .../hosts/max/plugins/load/load_pointcache.py | 8 ++---- .../hosts/max/plugins/load/load_pointcloud.py | 4 +-- .../max/plugins/load/load_redshift_proxy.py | 4 +-- 9 files changed, 22 insertions(+), 48 deletions(-) diff --git a/openpype/hosts/max/api/pipeline.py b/openpype/hosts/max/api/pipeline.py index 36c29ddbbb..602b506ef0 100644 --- a/openpype/hosts/max/api/pipeline.py +++ b/openpype/hosts/max/api/pipeline.py @@ -174,24 +174,9 @@ def containerise(name: str, nodes: list, context, loader=None, suffix="_CON"): return container -def load_OpenpypeData(container, loaded_nodes): - """Function to load the OpenpypeData Parameter along with - the published objects - - Args: - container (str): target container to set up - the custom attributes - loaded_nodes (list): list of nodes to be loaded +def load_OpenpypeData(): + """Re-loading the Openpype parameter built by the creator + Returns: + attribute: re-loading the custom OP attributes set in Maxscript """ - attrs = rt.Execute(MS_CUSTOM_ATTRIB) - if rt.custAttributes.get(container.baseObject, attrs): - rt.custAttributes.delete(container.baseObject, attrs) - rt.custAttributes.add(container.baseObject, attrs) - node_list = [] - for i in loaded_nodes: - node_ref = rt.NodeTransformMonitor(node=i) - node_list.append(node_ref) - - # Setting the property - rt.setProperty( - container.modifiers[0].openPypeData, "all_handles", node_list) + return rt.Execute(MS_CUSTOM_ATTRIB) diff --git a/openpype/hosts/max/plugins/load/load_camera_fbx.py b/openpype/hosts/max/plugins/load/load_camera_fbx.py index 6b16bfe474..e7aa482b2e 100644 --- a/openpype/hosts/max/plugins/load/load_camera_fbx.py +++ b/openpype/hosts/max/plugins/load/load_camera_fbx.py @@ -33,7 +33,7 @@ class FbxLoader(load.LoaderPlugin): container = rt.Container() container.name = f"{name}" selections = rt.GetCurrentSelection() - load_OpenpypeData(container, selections) + load_OpenpypeData() for selection in selections: selection.Parent = container @@ -52,7 +52,7 @@ class FbxLoader(load.LoaderPlugin): rt.FBXImporterSetParam("Preserveinstances", True) rt.ImportFile( path, rt.name("noPrompt"), using=rt.FBXIMP) - load_OpenpypeData(node, node.Children) + load_OpenpypeData() with maintained_selection(): rt.Select(node) diff --git a/openpype/hosts/max/plugins/load/load_model.py b/openpype/hosts/max/plugins/load/load_model.py index efd758063d..e987e5e900 100644 --- a/openpype/hosts/max/plugins/load/load_model.py +++ b/openpype/hosts/max/plugins/load/load_model.py @@ -45,10 +45,7 @@ class ModelAbcLoader(load.LoaderPlugin): self.log.error("Something failed when loading.") abc_container = abc_containers.pop() - selections = rt.GetCurrentSelection() - abc_selections = [abc for abc in selections - if abc.name != "Alembic"] - load_OpenpypeData(abc_container, abc_selections) + load_OpenpypeData() return containerise( name, [abc_container], context, loader=self.__class__.__name__ ) @@ -61,7 +58,6 @@ class ModelAbcLoader(load.LoaderPlugin): rt.Select(node.Children) nodes_list = [] - abc_object = None with maintained_selection(): rt.Select(node) @@ -77,9 +73,8 @@ class ModelAbcLoader(load.LoaderPlugin): alembic_obj = rt.GetNodeByName(abc_obj.name) alembic_obj.source = path nodes_list.append(alembic_obj) - abc_selections = [abc for abc in nodes_list - if abc.name != "Alembic"] - load_OpenpypeData(abc_object, abc_selections) + + load_OpenpypeData() lib.imprint( container["instance_node"], diff --git a/openpype/hosts/max/plugins/load/load_model_fbx.py b/openpype/hosts/max/plugins/load/load_model_fbx.py index 8f2b4f4ac3..76c2639388 100644 --- a/openpype/hosts/max/plugins/load/load_model_fbx.py +++ b/openpype/hosts/max/plugins/load/load_model_fbx.py @@ -29,7 +29,7 @@ class FbxModelLoader(load.LoaderPlugin): container.name = name selections = rt.GetCurrentSelection() - load_OpenpypeData(container, selections) + load_OpenpypeData() for selection in selections: selection.Parent = container @@ -50,7 +50,7 @@ class FbxModelLoader(load.LoaderPlugin): rt.FBXImporterSetParam("UpAxis", "Y") rt.FBXImporterSetParam("Preserveinstances", True) rt.importFile(path, rt.name("noPrompt"), using=rt.FBXIMP) - load_OpenpypeData(container, node.Children) + load_OpenpypeData() with maintained_selection(): rt.Select(node) diff --git a/openpype/hosts/max/plugins/load/load_model_obj.py b/openpype/hosts/max/plugins/load/load_model_obj.py index 83b5ec49b9..5a7181f438 100644 --- a/openpype/hosts/max/plugins/load/load_model_obj.py +++ b/openpype/hosts/max/plugins/load/load_model_obj.py @@ -26,7 +26,7 @@ class ObjLoader(load.LoaderPlugin): container = rt.Container() container.name = name selections = rt.GetCurrentSelection() - load_OpenpypeData(container, selections) + load_OpenpypeData() # get current selection for selection in selections: selection.Parent = container @@ -53,7 +53,7 @@ class ObjLoader(load.LoaderPlugin): selections = rt.GetCurrentSelection() for selection in selections: selection.Parent = container - load_OpenpypeData(container, container.Children) + load_OpenpypeData() with maintained_selection(): rt.Select(node) diff --git a/openpype/hosts/max/plugins/load/load_model_usd.py b/openpype/hosts/max/plugins/load/load_model_usd.py index a1961e6d89..0e275dd02e 100644 --- a/openpype/hosts/max/plugins/load/load_model_usd.py +++ b/openpype/hosts/max/plugins/load/load_model_usd.py @@ -30,10 +30,8 @@ class ModelUSDLoader(load.LoaderPlugin): rt.LogLevel = rt.Name("info") rt.USDImporter.importFile(filepath, importOptions=import_options) - selections = rt.GetCurrentSelection() asset = rt.GetNodeByName(name) - mesh_selections = [r for r in selections if r != asset] - load_OpenpypeData(asset, mesh_selections) + load_OpenpypeData() return containerise( name, [asset], context, loader=self.__class__.__name__) @@ -62,7 +60,7 @@ class ModelUSDLoader(load.LoaderPlugin): asset = rt.GetNodeByName(instance_name) asset.Parent = node - load_OpenpypeData(asset, asset.Children) + load_OpenpypeData() with maintained_selection(): rt.Select(node) diff --git a/openpype/hosts/max/plugins/load/load_pointcache.py b/openpype/hosts/max/plugins/load/load_pointcache.py index 7af588566e..dda57add69 100644 --- a/openpype/hosts/max/plugins/load/load_pointcache.py +++ b/openpype/hosts/max/plugins/load/load_pointcache.py @@ -49,9 +49,7 @@ class AbcLoader(load.LoaderPlugin): abc_container = abc_containers.pop() selections = rt.GetCurrentSelection() - abc_selections = [abc for abc in selections - if abc.name != "Alembic"] - load_OpenpypeData(abc_container, abc_selections) + load_OpenpypeData() for abc in selections: for cam_shape in abc.Children: cam_shape.playbackType = 2 @@ -72,7 +70,6 @@ class AbcLoader(load.LoaderPlugin): {"representation": str(representation["_id"])}, ) nodes_list = [] - abc_object = None with maintained_selection(): rt.Select(node.Children) @@ -88,8 +85,7 @@ class AbcLoader(load.LoaderPlugin): alembic_obj = rt.GetNodeByName(abc_obj.name) alembic_obj.source = path nodes_list.append(alembic_obj) - abc_selections = [abc for abc in nodes_list if abc.name != "Alembic"] - load_OpenpypeData(abc_object, abc_selections) + load_OpenpypeData() def switch(self, container, representation): self.update(container, representation) diff --git a/openpype/hosts/max/plugins/load/load_pointcloud.py b/openpype/hosts/max/plugins/load/load_pointcloud.py index 18998f4529..8ab81d79e7 100644 --- a/openpype/hosts/max/plugins/load/load_pointcloud.py +++ b/openpype/hosts/max/plugins/load/load_pointcloud.py @@ -25,7 +25,7 @@ class PointCloudLoader(load.LoaderPlugin): prt_container = rt.container() prt_container.name = name obj.Parent = prt_container - load_OpenpypeData(prt_container, [obj]) + load_OpenpypeData() return containerise( name, [prt_container], context, loader=self.__class__.__name__) @@ -41,7 +41,7 @@ class PointCloudLoader(load.LoaderPlugin): for prt in rt.Selection: prt_object = rt.GetNodeByName(prt.name) prt_object.filename = path - load_OpenpypeData(node, node.Children) + load_OpenpypeData() lib.imprint(container["instance_node"], { "representation": str(representation["_id"]) }) diff --git a/openpype/hosts/max/plugins/load/load_redshift_proxy.py b/openpype/hosts/max/plugins/load/load_redshift_proxy.py index b62400d2e5..23f78d0629 100644 --- a/openpype/hosts/max/plugins/load/load_redshift_proxy.py +++ b/openpype/hosts/max/plugins/load/load_redshift_proxy.py @@ -33,7 +33,7 @@ class RedshiftProxyLoader(load.LoaderPlugin): container = rt.container() container.name = name rs_proxy.Parent = container - load_OpenpypeData(container, [rs_proxy]) + load_OpenpypeData() asset = rt.getNodeByName(name) return containerise( @@ -49,7 +49,7 @@ class RedshiftProxyLoader(load.LoaderPlugin): for proxy in children_node.Children: proxy.file = path - load_OpenpypeData(node, node.Children) + load_OpenpypeData() lib.imprint(container["instance_node"], { "representation": str(representation["_id"]) }) From fdad1a48b0cc0f460df53b6c9dc101a24b98d656 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 14 Aug 2023 18:09:02 +0800 Subject: [PATCH 104/327] Hound --- openpype/hosts/max/plugins/load/load_model.py | 2 -- openpype/hosts/max/plugins/load/load_pointcache.py | 1 - 2 files changed, 3 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_model.py b/openpype/hosts/max/plugins/load/load_model.py index e987e5e900..7ba048c5e7 100644 --- a/openpype/hosts/max/plugins/load/load_model.py +++ b/openpype/hosts/max/plugins/load/load_model.py @@ -60,13 +60,11 @@ class ModelAbcLoader(load.LoaderPlugin): nodes_list = [] with maintained_selection(): rt.Select(node) - for alembic in rt.Selection: abc = rt.GetNodeByName(alembic.name) rt.Select(abc.Children) for abc_con in rt.Selection: container = rt.GetNodeByName(abc_con.name) - abc_object = container container.source = path rt.Select(container.Children) for abc_obj in rt.Selection: diff --git a/openpype/hosts/max/plugins/load/load_pointcache.py b/openpype/hosts/max/plugins/load/load_pointcache.py index dda57add69..ec379e39f7 100644 --- a/openpype/hosts/max/plugins/load/load_pointcache.py +++ b/openpype/hosts/max/plugins/load/load_pointcache.py @@ -78,7 +78,6 @@ class AbcLoader(load.LoaderPlugin): rt.Select(abc.Children) for abc_con in rt.Selection: container = rt.GetNodeByName(abc_con.name) - abc_object = container container.source = path rt.Select(container.Children) for abc_obj in rt.Selection: From 256ffee407e8718ca6099c5f7185d0ec88a0ace7 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Mon, 14 Aug 2023 15:54:14 +0300 Subject: [PATCH 105/327] resolve some conversations --- openpype/pipeline/colorspace.py | 2 +- openpype/scripts/ocio_wrapper.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 392089237b..a0efb5e18c 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -604,7 +604,7 @@ def _get_imageio_settings(project_settings, host_name): def get_display_view_colorspace_name(config_path, display, view): - """get view colorspace name for the given display and view. + """Return colorspace name for the given display and view. Args: config_path (str): path string leading to config.ocio diff --git a/openpype/scripts/ocio_wrapper.py b/openpype/scripts/ocio_wrapper.py index 556568ce20..e491206ebb 100644 --- a/openpype/scripts/ocio_wrapper.py +++ b/openpype/scripts/ocio_wrapper.py @@ -247,7 +247,7 @@ def get_display_view_colorspace_name(in_path, out_path, with open(json_path, "w") as f: json.dump(out_data, f) - print(f"Viewer data are saved to '{json_path}'") + print(f"Display view colorspace saved to '{json_path}'") if __name__ == '__main__': main() From e666ff641b4e80dc5aaff837fffd2b0f9651f6c2 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 15 Aug 2023 17:14:42 +0800 Subject: [PATCH 106/327] reload the modifiers to the container with OP Data --- openpype/hosts/max/api/pipeline.py | 27 +++++++++++++++++++ .../hosts/max/plugins/load/load_camera_fbx.py | 18 +++++++------ openpype/hosts/max/plugins/load/load_model.py | 11 ++++---- .../hosts/max/plugins/load/load_model_fbx.py | 13 ++++++--- .../hosts/max/plugins/load/load_model_obj.py | 6 ++--- .../hosts/max/plugins/load/load_model_usd.py | 8 +++--- .../hosts/max/plugins/load/load_pointcache.py | 11 +++++--- .../hosts/max/plugins/load/load_pointcloud.py | 8 +++--- .../max/plugins/load/load_redshift_proxy.py | 8 +++--- 9 files changed, 78 insertions(+), 32 deletions(-) diff --git a/openpype/hosts/max/api/pipeline.py b/openpype/hosts/max/api/pipeline.py index 602b506ef0..6b02f06b85 100644 --- a/openpype/hosts/max/api/pipeline.py +++ b/openpype/hosts/max/api/pipeline.py @@ -180,3 +180,30 @@ def load_OpenpypeData(): attribute: re-loading the custom OP attributes set in Maxscript """ return rt.Execute(MS_CUSTOM_ATTRIB) + + +def import_OpenpypeData(container, selections): + attrs = load_OpenpypeData() + modifier = rt.EmptyModifier() + rt.addModifier(container, modifier) + container.modifiers[0].name = "OP Data" + rt.custAttributes.add(container.modifiers[0], attrs) + node_list = [] + sel_list = [] + for i in selections: + node_ref = rt.NodeTransformMonitor(node=i) + node_list.append(node_ref) + sel_list.append(str(i)) + # Setting the property + rt.setProperty( + container.modifiers[0].openPypeData, + "all_handles", node_list) + rt.setProperty( + container.modifiers[0].openPypeData, + "sel_list", sel_list) + + +def update_Openpype_Data(container, selections): + if container.modifiers[0].name == "OP Data": + rt.deleteModifier(container, container.modifiers[0]) + import_OpenpypeData(container, selections) diff --git a/openpype/hosts/max/plugins/load/load_camera_fbx.py b/openpype/hosts/max/plugins/load/load_camera_fbx.py index e7aa482b2e..7bd02e4615 100644 --- a/openpype/hosts/max/plugins/load/load_camera_fbx.py +++ b/openpype/hosts/max/plugins/load/load_camera_fbx.py @@ -1,7 +1,9 @@ import os from openpype.hosts.max.api import lib, maintained_selection -from openpype.hosts.max.api.pipeline import containerise, load_OpenpypeData +from openpype.hosts.max.api.pipeline import ( + containerise, import_OpenpypeData, update_Openpype_Data +) from openpype.pipeline import get_representation_path, load @@ -16,24 +18,21 @@ class FbxLoader(load.LoaderPlugin): def load(self, context, name=None, namespace=None, data=None): from pymxs import runtime as rt - filepath = self.filepath_from_context(context) filepath = os.path.normpath(filepath) rt.FBXImporterSetParam("Animation", True) rt.FBXImporterSetParam("Camera", True) rt.FBXImporterSetParam("AxisConversionMethod", True) + rt.FBXImporterSetParam("Mode", rt.Name("create")) rt.FBXImporterSetParam("Preserveinstances", True) rt.ImportFile( filepath, rt.name("noPrompt"), using=rt.FBXIMP) - container = rt.GetNodeByName(f"{name}") - if not container: - container = rt.Container() - container.name = f"{name}" + container = rt.container(name=name) selections = rt.GetCurrentSelection() - load_OpenpypeData() + import_OpenpypeData(container, selections) for selection in selections: selection.Parent = container @@ -45,14 +44,17 @@ class FbxLoader(load.LoaderPlugin): path = get_representation_path(representation) node = rt.GetNodeByName(container["instance_node"]) + inst_name, _ = os.path.split(container["instance_node"]) + container = rt.getNodeByName(inst_name) rt.Select(node.Children) + update_Openpype_Data(container, rt.GetCurrentSelection()) rt.FBXImporterSetParam("Animation", True) rt.FBXImporterSetParam("Camera", True) + rt.FBXImporterSetParam("Mode", rt.Name("merge")) rt.FBXImporterSetParam("AxisConversionMethod", True) rt.FBXImporterSetParam("Preserveinstances", True) rt.ImportFile( path, rt.name("noPrompt"), using=rt.FBXIMP) - load_OpenpypeData() with maintained_selection(): rt.Select(node) diff --git a/openpype/hosts/max/plugins/load/load_model.py b/openpype/hosts/max/plugins/load/load_model.py index 7ba048c5e7..ea60c33c19 100644 --- a/openpype/hosts/max/plugins/load/load_model.py +++ b/openpype/hosts/max/plugins/load/load_model.py @@ -1,6 +1,8 @@ import os from openpype.pipeline import load, get_representation_path -from openpype.hosts.max.api.pipeline import containerise, load_OpenpypeData +from openpype.hosts.max.api.pipeline import ( + containerise, import_OpenpypeData, update_Openpype_Data +) from openpype.hosts.max.api import lib from openpype.hosts.max.api.lib import maintained_selection @@ -30,7 +32,7 @@ class ModelAbcLoader(load.LoaderPlugin): rt.AlembicImport.CustomAttributes = True rt.AlembicImport.UVs = True rt.AlembicImport.VertexColors = True - rt.importFile(file_path, rt.name("noPrompt")) + rt.importFile(file_path, rt.name("noPrompt"), using=rt.AlembicImport) abc_after = { c @@ -45,7 +47,7 @@ class ModelAbcLoader(load.LoaderPlugin): self.log.error("Something failed when loading.") abc_container = abc_containers.pop() - load_OpenpypeData() + import_OpenpypeData(abc_container, abc_container.Children) return containerise( name, [abc_container], context, loader=self.__class__.__name__ ) @@ -62,6 +64,7 @@ class ModelAbcLoader(load.LoaderPlugin): rt.Select(node) for alembic in rt.Selection: abc = rt.GetNodeByName(alembic.name) + import_OpenpypeData(abc, abc.Children) rt.Select(abc.Children) for abc_con in rt.Selection: container = rt.GetNodeByName(abc_con.name) @@ -72,8 +75,6 @@ class ModelAbcLoader(load.LoaderPlugin): alembic_obj.source = path nodes_list.append(alembic_obj) - load_OpenpypeData() - lib.imprint( container["instance_node"], {"representation": str(representation["_id"])}, diff --git a/openpype/hosts/max/plugins/load/load_model_fbx.py b/openpype/hosts/max/plugins/load/load_model_fbx.py index 76c2639388..9f80875d5b 100644 --- a/openpype/hosts/max/plugins/load/load_model_fbx.py +++ b/openpype/hosts/max/plugins/load/load_model_fbx.py @@ -1,6 +1,8 @@ import os from openpype.pipeline import load, get_representation_path -from openpype.hosts.max.api.pipeline import containerise, load_OpenpypeData +from openpype.hosts.max.api.pipeline import ( + containerise, import_OpenpypeData, update_Openpype_Data +) from openpype.hosts.max.api import lib from openpype.hosts.max.api.lib import maintained_selection @@ -20,6 +22,7 @@ class FbxModelLoader(load.LoaderPlugin): filepath = os.path.normpath(self.filepath_from_context(context)) rt.FBXImporterSetParam("Animation", False) rt.FBXImporterSetParam("Cameras", False) + rt.FBXImporterSetParam("Mode", rt.Name("create")) rt.FBXImporterSetParam("Preserveinstances", True) rt.importFile(filepath, rt.name("noPrompt"), using=rt.FBXIMP) @@ -29,7 +32,7 @@ class FbxModelLoader(load.LoaderPlugin): container.name = name selections = rt.GetCurrentSelection() - load_OpenpypeData() + import_OpenpypeData(container, selections) for selection in selections: selection.Parent = container @@ -42,15 +45,19 @@ class FbxModelLoader(load.LoaderPlugin): from pymxs import runtime as rt path = get_representation_path(representation) node = rt.getNodeByName(container["instance_node"]) + inst_name, _ = os.path.splitext(container["instance_node"]) rt.select(node.Children) rt.FBXImporterSetParam("Animation", False) rt.FBXImporterSetParam("Cameras", False) + rt.FBXImporterSetParam("Mode", rt.Name("merge")) rt.FBXImporterSetParam("AxisConversionMethod", True) rt.FBXImporterSetParam("UpAxis", "Y") rt.FBXImporterSetParam("Preserveinstances", True) rt.importFile(path, rt.name("noPrompt"), using=rt.FBXIMP) - load_OpenpypeData() + + container = rt.getNodeByName(inst_name) + update_Openpype_Data(container, rt.GetCurrentSelection()) with maintained_selection(): rt.Select(node) diff --git a/openpype/hosts/max/plugins/load/load_model_obj.py b/openpype/hosts/max/plugins/load/load_model_obj.py index 5a7181f438..f4791bfbb3 100644 --- a/openpype/hosts/max/plugins/load/load_model_obj.py +++ b/openpype/hosts/max/plugins/load/load_model_obj.py @@ -2,7 +2,7 @@ import os from openpype.hosts.max.api import lib from openpype.hosts.max.api.lib import maintained_selection -from openpype.hosts.max.api.pipeline import containerise, load_OpenpypeData +from openpype.hosts.max.api.pipeline import containerise, import_OpenpypeData from openpype.pipeline import get_representation_path, load @@ -26,7 +26,7 @@ class ObjLoader(load.LoaderPlugin): container = rt.Container() container.name = name selections = rt.GetCurrentSelection() - load_OpenpypeData() + import_OpenpypeData(container, selections) # get current selection for selection in selections: selection.Parent = container @@ -53,7 +53,7 @@ class ObjLoader(load.LoaderPlugin): selections = rt.GetCurrentSelection() for selection in selections: selection.Parent = container - load_OpenpypeData() + import_OpenpypeData(container, selections) with maintained_selection(): rt.Select(node) diff --git a/openpype/hosts/max/plugins/load/load_model_usd.py b/openpype/hosts/max/plugins/load/load_model_usd.py index 0e275dd02e..96b5cdedf0 100644 --- a/openpype/hosts/max/plugins/load/load_model_usd.py +++ b/openpype/hosts/max/plugins/load/load_model_usd.py @@ -2,7 +2,9 @@ import os from openpype.hosts.max.api import lib from openpype.hosts.max.api.lib import maintained_selection -from openpype.hosts.max.api.pipeline import containerise, load_OpenpypeData +from openpype.hosts.max.api.pipeline import ( + containerise, import_OpenpypeData, update_Openpype_Data +) from openpype.pipeline import get_representation_path, load @@ -31,7 +33,7 @@ class ModelUSDLoader(load.LoaderPlugin): rt.USDImporter.importFile(filepath, importOptions=import_options) asset = rt.GetNodeByName(name) - load_OpenpypeData() + import_OpenpypeData(asset, asset.Children) return containerise( name, [asset], context, loader=self.__class__.__name__) @@ -60,7 +62,7 @@ class ModelUSDLoader(load.LoaderPlugin): asset = rt.GetNodeByName(instance_name) asset.Parent = node - load_OpenpypeData() + update_Openpype_Data(asset, asset.Children) with maintained_selection(): rt.Select(node) diff --git a/openpype/hosts/max/plugins/load/load_pointcache.py b/openpype/hosts/max/plugins/load/load_pointcache.py index ec379e39f7..18a68732e9 100644 --- a/openpype/hosts/max/plugins/load/load_pointcache.py +++ b/openpype/hosts/max/plugins/load/load_pointcache.py @@ -7,7 +7,9 @@ Because of limited api, alembics can be only loaded, but not easily updated. import os from openpype.pipeline import load, get_representation_path from openpype.hosts.max.api import lib, maintained_selection -from openpype.hosts.max.api.pipeline import containerise, load_OpenpypeData +from openpype.hosts.max.api.pipeline import ( + containerise, import_OpenpypeData, update_Openpype_Data +) class AbcLoader(load.LoaderPlugin): @@ -33,7 +35,7 @@ class AbcLoader(load.LoaderPlugin): } rt.AlembicImport.ImportToRoot = False - rt.importFile(file_path, rt.name("noPrompt")) + rt.importFile(file_path, rt.name("noPrompt"), using=rt.AlembicImport) abc_after = { c @@ -49,7 +51,7 @@ class AbcLoader(load.LoaderPlugin): abc_container = abc_containers.pop() selections = rt.GetCurrentSelection() - load_OpenpypeData() + import_OpenpypeData(abc_container, abc_container.Children) for abc in selections: for cam_shape in abc.Children: cam_shape.playbackType = 2 @@ -75,6 +77,7 @@ class AbcLoader(load.LoaderPlugin): for alembic in rt.Selection: abc = rt.GetNodeByName(alembic.name) + update_Openpype_Data(abc, abc.Children) rt.Select(abc.Children) for abc_con in rt.Selection: container = rt.GetNodeByName(abc_con.name) @@ -84,7 +87,7 @@ class AbcLoader(load.LoaderPlugin): alembic_obj = rt.GetNodeByName(abc_obj.name) alembic_obj.source = path nodes_list.append(alembic_obj) - load_OpenpypeData() + def switch(self, container, representation): self.update(container, representation) diff --git a/openpype/hosts/max/plugins/load/load_pointcloud.py b/openpype/hosts/max/plugins/load/load_pointcloud.py index 8ab81d79e7..2f41173bce 100644 --- a/openpype/hosts/max/plugins/load/load_pointcloud.py +++ b/openpype/hosts/max/plugins/load/load_pointcloud.py @@ -1,7 +1,9 @@ import os from openpype.hosts.max.api import lib, maintained_selection -from openpype.hosts.max.api.pipeline import containerise, load_OpenpypeData +from openpype.hosts.max.api.pipeline import ( + containerise, import_OpenpypeData, update_Openpype_Data +) from openpype.pipeline import get_representation_path, load @@ -25,7 +27,7 @@ class PointCloudLoader(load.LoaderPlugin): prt_container = rt.container() prt_container.name = name obj.Parent = prt_container - load_OpenpypeData() + import_OpenpypeData(prt_container, [obj]) return containerise( name, [prt_container], context, loader=self.__class__.__name__) @@ -41,7 +43,7 @@ class PointCloudLoader(load.LoaderPlugin): for prt in rt.Selection: prt_object = rt.GetNodeByName(prt.name) prt_object.filename = path - load_OpenpypeData() + update_Openpype_Data(node, node.Children) lib.imprint(container["instance_node"], { "representation": str(representation["_id"]) }) diff --git a/openpype/hosts/max/plugins/load/load_redshift_proxy.py b/openpype/hosts/max/plugins/load/load_redshift_proxy.py index 23f78d0629..4b488bcb7c 100644 --- a/openpype/hosts/max/plugins/load/load_redshift_proxy.py +++ b/openpype/hosts/max/plugins/load/load_redshift_proxy.py @@ -5,7 +5,9 @@ from openpype.pipeline import ( load, get_representation_path ) -from openpype.hosts.max.api.pipeline import containerise, load_OpenpypeData +from openpype.hosts.max.api.pipeline import ( + containerise, import_OpenpypeData, update_Openpype_Data +) from openpype.hosts.max.api import lib @@ -33,7 +35,7 @@ class RedshiftProxyLoader(load.LoaderPlugin): container = rt.container() container.name = name rs_proxy.Parent = container - load_OpenpypeData() + import_OpenpypeData(container, [rs_proxy]) asset = rt.getNodeByName(name) return containerise( @@ -49,7 +51,7 @@ class RedshiftProxyLoader(load.LoaderPlugin): for proxy in children_node.Children: proxy.file = path - load_OpenpypeData() + update_Openpype_Data(node, node.Children) lib.imprint(container["instance_node"], { "representation": str(representation["_id"]) }) From 5f54f9082477162d65d9944365a285e732fad0fd Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 15 Aug 2023 17:16:14 +0800 Subject: [PATCH 107/327] reload the moddifier with OP Data in load model --- openpype/hosts/max/plugins/load/load_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/load/load_model.py b/openpype/hosts/max/plugins/load/load_model.py index ea60c33c19..e1978e35ad 100644 --- a/openpype/hosts/max/plugins/load/load_model.py +++ b/openpype/hosts/max/plugins/load/load_model.py @@ -64,7 +64,7 @@ class ModelAbcLoader(load.LoaderPlugin): rt.Select(node) for alembic in rt.Selection: abc = rt.GetNodeByName(alembic.name) - import_OpenpypeData(abc, abc.Children) + update_Openpype_Data(abc, abc.Children) rt.Select(abc.Children) for abc_con in rt.Selection: container = rt.GetNodeByName(abc_con.name) From 04b36e961180e455605dd513f626895b8e818f31 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 15 Aug 2023 12:52:56 +0200 Subject: [PATCH 108/327] fix provider icons access (#5450) --- openpype/tools/sceneinventory/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/sceneinventory/model.py b/openpype/tools/sceneinventory/model.py index 64c439712c..4fd82f04a4 100644 --- a/openpype/tools/sceneinventory/model.py +++ b/openpype/tools/sceneinventory/model.py @@ -85,7 +85,7 @@ class InventoryModel(TreeModel): self.remote_provider = remote_provider self._site_icons = { provider: QtGui.QIcon(icon_path) - for provider, icon_path in self.get_site_icons().items() + for provider, icon_path in sync_server.get_site_icons().items() } if "active_site" not in self.Columns: self.Columns.append("active_site") From cf565a205e9c3aaa7ae54ab729d74b4111e89a11 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 15 Aug 2023 13:36:40 +0200 Subject: [PATCH 109/327] Chore: Default variant in create plugin (#5429) * define constant 'DEFAULT_VARIANT_VALUE' * 'get_default_variant' always returns string * added 'default_variant' property for backwards compatibility * added more options to receive default variant * added backwards compatibility for 'default_variant' attribute * better autofix for backwards compatibility * use 'DEFAULT_VARIANT_VALUE' in publisher UI * fix docstring * Use 'Main' instead of 'main' for default variant --- openpype/pipeline/create/__init__.py | 2 + openpype/pipeline/create/constants.py | 2 + openpype/pipeline/create/creator_plugins.py | 79 ++++++++++++++++--- .../tools/publisher/widgets/create_widget.py | 5 +- 4 files changed, 77 insertions(+), 11 deletions(-) diff --git a/openpype/pipeline/create/__init__.py b/openpype/pipeline/create/__init__.py index 6755224c19..94d575a776 100644 --- a/openpype/pipeline/create/__init__.py +++ b/openpype/pipeline/create/__init__.py @@ -2,6 +2,7 @@ from .constants import ( SUBSET_NAME_ALLOWED_SYMBOLS, DEFAULT_SUBSET_TEMPLATE, PRE_CREATE_THUMBNAIL_KEY, + DEFAULT_VARIANT_VALUE, ) from .utils import ( @@ -50,6 +51,7 @@ __all__ = ( "SUBSET_NAME_ALLOWED_SYMBOLS", "DEFAULT_SUBSET_TEMPLATE", "PRE_CREATE_THUMBNAIL_KEY", + "DEFAULT_VARIANT_VALUE", "get_last_versions_for_instances", "get_next_versions_for_instances", diff --git a/openpype/pipeline/create/constants.py b/openpype/pipeline/create/constants.py index 375cfc4a12..7d1d0154e9 100644 --- a/openpype/pipeline/create/constants.py +++ b/openpype/pipeline/create/constants.py @@ -1,10 +1,12 @@ SUBSET_NAME_ALLOWED_SYMBOLS = "a-zA-Z0-9_." DEFAULT_SUBSET_TEMPLATE = "{family}{Variant}" PRE_CREATE_THUMBNAIL_KEY = "thumbnail_source" +DEFAULT_VARIANT_VALUE = "Main" __all__ = ( "SUBSET_NAME_ALLOWED_SYMBOLS", "DEFAULT_SUBSET_TEMPLATE", "PRE_CREATE_THUMBNAIL_KEY", + "DEFAULT_VARIANT_VALUE", ) diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index c9edbbfd71..38d6b6f465 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 @@ -20,6 +19,7 @@ from openpype.pipeline.plugin_discover import ( deregister_plugin_path ) +from .constants import DEFAULT_VARIANT_VALUE from .subset_name import get_subset_name from .utils import get_next_versions_for_instances from .legacy_create import LegacyCreator @@ -517,7 +517,7 @@ class Creator(BaseCreator): default_variants = [] # Default variant used in 'get_default_variant' - default_variant = None + _default_variant = None # Short description of family # - may not be used if `get_description` is overriden @@ -543,6 +543,21 @@ class Creator(BaseCreator): # - similar to instance attribute definitions pre_create_attr_defs = [] + def __init__(self, *args, **kwargs): + cls = self.__class__ + + # Fix backwards compatibility for plugins which override + # 'default_variant' attribute directly + if not isinstance(cls.default_variant, property): + # Move value from 'default_variant' to '_default_variant' + self._default_variant = self.default_variant + # Create property 'default_variant' on the class + cls.default_variant = property( + cls._get_default_variant_wrap, + cls._set_default_variant_wrap + ) + super(Creator, self).__init__(*args, **kwargs) + @property def show_order(self): """Order in which is creator shown in UI. @@ -595,10 +610,10 @@ class Creator(BaseCreator): def get_default_variants(self): """Default variant values for UI tooltips. - Replacement of `defatults` attribute. Using method gives ability to - have some "logic" other than attribute values. + Replacement of `default_variants` attribute. Using method gives + ability to have some "logic" other than attribute values. - By default returns `default_variants` value. + By default, returns `default_variants` value. Returns: List[str]: Whisper variants for user input. @@ -606,17 +621,63 @@ class Creator(BaseCreator): return copy.deepcopy(self.default_variants) - def get_default_variant(self): + def get_default_variant(self, only_explicit=False): """Default variant value that will be used to prefill variant input. This is for user input and value may not be content of result from `get_default_variants`. - Can return `None`. In that case first element from - `get_default_variants` should be used. + Note: + This method does not allow to have empty string as + default variant. + + Args: + only_explicit (Optional[bool]): If True, only explicit default + variant from '_default_variant' will be returned. + + Returns: + str: Variant value. """ - return self.default_variant + if only_explicit or self._default_variant: + return self._default_variant + + for variant in self.get_default_variants(): + return variant + return DEFAULT_VARIANT_VALUE + + def _get_default_variant_wrap(self): + """Default variant value that will be used to prefill variant input. + + Wrapper for 'get_default_variant'. + + Notes: + This method is wrapper for 'get_default_variant' + for 'default_variant' property, so creator can override + the method. + + Returns: + str: Variant value. + """ + + return self.get_default_variant() + + def _set_default_variant_wrap(self, variant): + """Set default variant value. + + This method is needed for automated settings overrides which are + changing attributes based on keys in settings. + + Args: + variant (str): New default variant value. + """ + + self._default_variant = variant + + default_variant = property( + _get_default_variant_wrap, + _set_default_variant_wrap + ) def get_pre_create_attr_defs(self): """Plugin attribute definitions needed for creation. diff --git a/openpype/tools/publisher/widgets/create_widget.py b/openpype/tools/publisher/widgets/create_widget.py index 1940d16eb8..64fed1d70c 100644 --- a/openpype/tools/publisher/widgets/create_widget.py +++ b/openpype/tools/publisher/widgets/create_widget.py @@ -6,6 +6,7 @@ from openpype import AYON_SERVER_ENABLED from openpype.pipeline.create import ( SUBSET_NAME_ALLOWED_SYMBOLS, PRE_CREATE_THUMBNAIL_KEY, + DEFAULT_VARIANT_VALUE, TaskNotSetError, ) @@ -626,7 +627,7 @@ class CreateWidget(QtWidgets.QWidget): default_variants = creator_item.default_variants if not default_variants: - default_variants = ["Main"] + default_variants = [DEFAULT_VARIANT_VALUE] default_variant = creator_item.default_variant if not default_variant: @@ -642,7 +643,7 @@ class CreateWidget(QtWidgets.QWidget): elif variant: self.variant_hints_menu.addAction(variant) - variant_text = default_variant or "Main" + variant_text = default_variant or DEFAULT_VARIANT_VALUE # Make sure subset name is updated to new plugin if variant_text == self.variant_input.text(): self._on_variant_change() From cf4ce6bbc5d2684f7fbbcc4b66865dd1be2ebc98 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 15 Aug 2023 22:36:28 +0800 Subject: [PATCH 110/327] rename the loadOpenpypedata functions --- openpype/hosts/max/api/pipeline.py | 27 ++++++++++++++----- .../hosts/max/plugins/load/load_camera_fbx.py | 9 ++++--- openpype/hosts/max/plugins/load/load_model.py | 9 ++++--- .../hosts/max/plugins/load/load_model_fbx.py | 7 ++--- .../hosts/max/plugins/load/load_model_obj.py | 10 ++++--- .../hosts/max/plugins/load/load_model_usd.py | 8 +++--- .../hosts/max/plugins/load/load_pointcache.py | 9 ++++--- .../hosts/max/plugins/load/load_pointcloud.py | 8 +++--- .../max/plugins/load/load_redshift_proxy.py | 8 +++--- 9 files changed, 65 insertions(+), 30 deletions(-) diff --git a/openpype/hosts/max/api/pipeline.py b/openpype/hosts/max/api/pipeline.py index 6b02f06b85..08ff5c6baf 100644 --- a/openpype/hosts/max/api/pipeline.py +++ b/openpype/hosts/max/api/pipeline.py @@ -174,16 +174,24 @@ def containerise(name: str, nodes: list, context, loader=None, suffix="_CON"): return container -def load_OpenpypeData(): - """Re-loading the Openpype parameter built by the creator +def load_custom_attribute_data(): + """Re-loading the Openpype/AYON custom parameter built by the creator + Returns: attribute: re-loading the custom OP attributes set in Maxscript """ return rt.Execute(MS_CUSTOM_ATTRIB) -def import_OpenpypeData(container, selections): - attrs = load_OpenpypeData() +def import_custom_attribute_data(container: str, selections: list): + """Importing the Openpype/AYON custom parameter built by the creator + + Args: + container (str): target container which adds custom attributes + selections (_type_): nodes to be added into + group in custom attributes + """ + attrs = load_custom_attribute_data() modifier = rt.EmptyModifier() rt.addModifier(container, modifier) container.modifiers[0].name = "OP Data" @@ -203,7 +211,14 @@ def import_OpenpypeData(container, selections): "sel_list", sel_list) -def update_Openpype_Data(container, selections): +def update_custom_attribute_data(container: str, selections: list): + """Updating the Openpype/AYON custom parameter built by the creator + + Args: + container (str): target container which adds custom attributes + selections (_type_): nodes to be added into + group in custom attributes + """ if container.modifiers[0].name == "OP Data": rt.deleteModifier(container, container.modifiers[0]) - import_OpenpypeData(container, selections) + import_custom_attribute_data(container, selections) diff --git a/openpype/hosts/max/plugins/load/load_camera_fbx.py b/openpype/hosts/max/plugins/load/load_camera_fbx.py index 7bd02e4615..1e4e5b3e91 100644 --- a/openpype/hosts/max/plugins/load/load_camera_fbx.py +++ b/openpype/hosts/max/plugins/load/load_camera_fbx.py @@ -2,7 +2,9 @@ import os from openpype.hosts.max.api import lib, maintained_selection from openpype.hosts.max.api.pipeline import ( - containerise, import_OpenpypeData, update_Openpype_Data + containerise, + import_custom_attribute_data, + update_custom_attribute_data ) from openpype.pipeline import get_representation_path, load @@ -32,7 +34,7 @@ class FbxLoader(load.LoaderPlugin): container = rt.container(name=name) selections = rt.GetCurrentSelection() - import_OpenpypeData(container, selections) + import_custom_attribute_data(container, selections) for selection in selections: selection.Parent = container @@ -47,7 +49,8 @@ class FbxLoader(load.LoaderPlugin): inst_name, _ = os.path.split(container["instance_node"]) container = rt.getNodeByName(inst_name) rt.Select(node.Children) - update_Openpype_Data(container, rt.GetCurrentSelection()) + update_custom_attribute_data( + container, rt.GetCurrentSelection()) rt.FBXImporterSetParam("Animation", True) rt.FBXImporterSetParam("Camera", True) rt.FBXImporterSetParam("Mode", rt.Name("merge")) diff --git a/openpype/hosts/max/plugins/load/load_model.py b/openpype/hosts/max/plugins/load/load_model.py index e1978e35ad..f71e4e8f7f 100644 --- a/openpype/hosts/max/plugins/load/load_model.py +++ b/openpype/hosts/max/plugins/load/load_model.py @@ -1,7 +1,9 @@ import os from openpype.pipeline import load, get_representation_path from openpype.hosts.max.api.pipeline import ( - containerise, import_OpenpypeData, update_Openpype_Data + containerise, + import_custom_attribute_data, + update_custom_attribute_data ) from openpype.hosts.max.api import lib from openpype.hosts.max.api.lib import maintained_selection @@ -47,7 +49,8 @@ class ModelAbcLoader(load.LoaderPlugin): self.log.error("Something failed when loading.") abc_container = abc_containers.pop() - import_OpenpypeData(abc_container, abc_container.Children) + import_custom_attribute_data( + abc_container, abc_container.Children) return containerise( name, [abc_container], context, loader=self.__class__.__name__ ) @@ -64,7 +67,7 @@ class ModelAbcLoader(load.LoaderPlugin): rt.Select(node) for alembic in rt.Selection: abc = rt.GetNodeByName(alembic.name) - update_Openpype_Data(abc, abc.Children) + update_custom_attribute_data(abc, abc.Children) rt.Select(abc.Children) for abc_con in rt.Selection: container = rt.GetNodeByName(abc_con.name) diff --git a/openpype/hosts/max/plugins/load/load_model_fbx.py b/openpype/hosts/max/plugins/load/load_model_fbx.py index 9f80875d5b..26520307c9 100644 --- a/openpype/hosts/max/plugins/load/load_model_fbx.py +++ b/openpype/hosts/max/plugins/load/load_model_fbx.py @@ -1,7 +1,7 @@ import os from openpype.pipeline import load, get_representation_path from openpype.hosts.max.api.pipeline import ( - containerise, import_OpenpypeData, update_Openpype_Data + containerise, import_custom_attribute_data, update_custom_attribute_data ) from openpype.hosts.max.api import lib from openpype.hosts.max.api.lib import maintained_selection @@ -32,7 +32,7 @@ class FbxModelLoader(load.LoaderPlugin): container.name = name selections = rt.GetCurrentSelection() - import_OpenpypeData(container, selections) + import_custom_attribute_data(container, selections) for selection in selections: selection.Parent = container @@ -57,7 +57,8 @@ class FbxModelLoader(load.LoaderPlugin): rt.importFile(path, rt.name("noPrompt"), using=rt.FBXIMP) container = rt.getNodeByName(inst_name) - update_Openpype_Data(container, rt.GetCurrentSelection()) + update_custom_attribute_data( + container, rt.GetCurrentSelection()) with maintained_selection(): rt.Select(node) diff --git a/openpype/hosts/max/plugins/load/load_model_obj.py b/openpype/hosts/max/plugins/load/load_model_obj.py index f4791bfbb3..05f37f9e5a 100644 --- a/openpype/hosts/max/plugins/load/load_model_obj.py +++ b/openpype/hosts/max/plugins/load/load_model_obj.py @@ -2,7 +2,11 @@ import os from openpype.hosts.max.api import lib from openpype.hosts.max.api.lib import maintained_selection -from openpype.hosts.max.api.pipeline import containerise, import_OpenpypeData +from openpype.hosts.max.api.pipeline import ( + containerise, + import_custom_attribute_data, + update_custom_attribute_data +) from openpype.pipeline import get_representation_path, load @@ -26,7 +30,7 @@ class ObjLoader(load.LoaderPlugin): container = rt.Container() container.name = name selections = rt.GetCurrentSelection() - import_OpenpypeData(container, selections) + import_custom_attribute_data(container, selections) # get current selection for selection in selections: selection.Parent = container @@ -53,7 +57,7 @@ class ObjLoader(load.LoaderPlugin): selections = rt.GetCurrentSelection() for selection in selections: selection.Parent = container - import_OpenpypeData(container, selections) + update_custom_attribute_data(container, selections) with maintained_selection(): rt.Select(node) diff --git a/openpype/hosts/max/plugins/load/load_model_usd.py b/openpype/hosts/max/plugins/load/load_model_usd.py index 96b5cdedf0..425b152278 100644 --- a/openpype/hosts/max/plugins/load/load_model_usd.py +++ b/openpype/hosts/max/plugins/load/load_model_usd.py @@ -3,7 +3,9 @@ import os from openpype.hosts.max.api import lib from openpype.hosts.max.api.lib import maintained_selection from openpype.hosts.max.api.pipeline import ( - containerise, import_OpenpypeData, update_Openpype_Data + containerise, + import_custom_attribute_data, + update_custom_attribute_data ) from openpype.pipeline import get_representation_path, load @@ -33,7 +35,7 @@ class ModelUSDLoader(load.LoaderPlugin): rt.USDImporter.importFile(filepath, importOptions=import_options) asset = rt.GetNodeByName(name) - import_OpenpypeData(asset, asset.Children) + import_custom_attribute_data(asset, asset.Children) return containerise( name, [asset], context, loader=self.__class__.__name__) @@ -62,7 +64,7 @@ class ModelUSDLoader(load.LoaderPlugin): asset = rt.GetNodeByName(instance_name) asset.Parent = node - update_Openpype_Data(asset, asset.Children) + update_custom_attribute_data(asset, asset.Children) with maintained_selection(): rt.Select(node) diff --git a/openpype/hosts/max/plugins/load/load_pointcache.py b/openpype/hosts/max/plugins/load/load_pointcache.py index 18a68732e9..0ec9fda3d5 100644 --- a/openpype/hosts/max/plugins/load/load_pointcache.py +++ b/openpype/hosts/max/plugins/load/load_pointcache.py @@ -8,7 +8,9 @@ import os from openpype.pipeline import load, get_representation_path from openpype.hosts.max.api import lib, maintained_selection from openpype.hosts.max.api.pipeline import ( - containerise, import_OpenpypeData, update_Openpype_Data + containerise, + import_custom_attribute_data, + update_custom_attribute_data ) @@ -51,7 +53,8 @@ class AbcLoader(load.LoaderPlugin): abc_container = abc_containers.pop() selections = rt.GetCurrentSelection() - import_OpenpypeData(abc_container, abc_container.Children) + import_custom_attribute_data( + abc_container, abc_container.Children) for abc in selections: for cam_shape in abc.Children: cam_shape.playbackType = 2 @@ -77,7 +80,7 @@ class AbcLoader(load.LoaderPlugin): for alembic in rt.Selection: abc = rt.GetNodeByName(alembic.name) - update_Openpype_Data(abc, abc.Children) + update_custom_attribute_data(abc, abc.Children) rt.Select(abc.Children) for abc_con in rt.Selection: container = rt.GetNodeByName(abc_con.name) diff --git a/openpype/hosts/max/plugins/load/load_pointcloud.py b/openpype/hosts/max/plugins/load/load_pointcloud.py index 2f41173bce..c263019beb 100644 --- a/openpype/hosts/max/plugins/load/load_pointcloud.py +++ b/openpype/hosts/max/plugins/load/load_pointcloud.py @@ -2,7 +2,9 @@ import os from openpype.hosts.max.api import lib, maintained_selection from openpype.hosts.max.api.pipeline import ( - containerise, import_OpenpypeData, update_Openpype_Data + containerise, + import_custom_attribute_data, + update_custom_attribute_data ) from openpype.pipeline import get_representation_path, load @@ -27,7 +29,7 @@ class PointCloudLoader(load.LoaderPlugin): prt_container = rt.container() prt_container.name = name obj.Parent = prt_container - import_OpenpypeData(prt_container, [obj]) + import_custom_attribute_data(prt_container, [obj]) return containerise( name, [prt_container], context, loader=self.__class__.__name__) @@ -43,7 +45,7 @@ class PointCloudLoader(load.LoaderPlugin): for prt in rt.Selection: prt_object = rt.GetNodeByName(prt.name) prt_object.filename = path - update_Openpype_Data(node, node.Children) + update_custom_attribute_data(node, node.Children) lib.imprint(container["instance_node"], { "representation": str(representation["_id"]) }) diff --git a/openpype/hosts/max/plugins/load/load_redshift_proxy.py b/openpype/hosts/max/plugins/load/load_redshift_proxy.py index 4b488bcb7c..6b100df611 100644 --- a/openpype/hosts/max/plugins/load/load_redshift_proxy.py +++ b/openpype/hosts/max/plugins/load/load_redshift_proxy.py @@ -6,7 +6,9 @@ from openpype.pipeline import ( get_representation_path ) from openpype.hosts.max.api.pipeline import ( - containerise, import_OpenpypeData, update_Openpype_Data + containerise, + import_custom_attribute_data, + update_custom_attribute_data ) from openpype.hosts.max.api import lib @@ -35,7 +37,7 @@ class RedshiftProxyLoader(load.LoaderPlugin): container = rt.container() container.name = name rs_proxy.Parent = container - import_OpenpypeData(container, [rs_proxy]) + import_custom_attribute_data(container, [rs_proxy]) asset = rt.getNodeByName(name) return containerise( @@ -51,7 +53,7 @@ class RedshiftProxyLoader(load.LoaderPlugin): for proxy in children_node.Children: proxy.file = path - update_Openpype_Data(node, node.Children) + update_custom_attribute_data(node, node.Children) lib.imprint(container["instance_node"], { "representation": str(representation["_id"]) }) From 4d96eff2ed7d272179337e65ed370b71ce2fa441 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 16 Aug 2023 03:24:46 +0000 Subject: [PATCH 111/327] [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 afbac53385..70eb32baff 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.16.4-nightly.1" +__version__ = "3.16.4-nightly.2" From bdc42761bdfbb06f0b167d7cf0ac49b87ced1a6e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 16 Aug 2023 03:25:32 +0000 Subject: [PATCH 112/327] 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 96fcc38d13..d2a4067a6a 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.16.4-nightly.2 - 3.16.4-nightly.1 - 3.16.3 - 3.16.3-nightly.5 @@ -134,7 +135,6 @@ body: - 3.14.7 - 3.14.7-nightly.8 - 3.14.7-nightly.7 - - 3.14.7-nightly.6 validations: required: true - type: dropdown From 8b0ba25c37d177b7b6a43f3536d3f98e9eb67898 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 16 Aug 2023 14:12:58 +0800 Subject: [PATCH 113/327] add load maxscene family --- .../hosts/max/plugins/load/load_max_scene.py | 44 ++++++++++++------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_max_scene.py b/openpype/hosts/max/plugins/load/load_max_scene.py index 76cd3bf367..637659ed44 100644 --- a/openpype/hosts/max/plugins/load/load_max_scene.py +++ b/openpype/hosts/max/plugins/load/load_max_scene.py @@ -1,7 +1,10 @@ import os from openpype.hosts.max.api import lib -from openpype.hosts.max.api.pipeline import containerise +from openpype.hosts.max.api.pipeline import ( + containerise, import_custom_attribute_data, + update_custom_attribute_data +) from openpype.pipeline import get_representation_path, load @@ -19,36 +22,43 @@ class MaxSceneLoader(load.LoaderPlugin): def load(self, context, name=None, namespace=None, data=None): from pymxs import runtime as rt - path = self.filepath_from_context(context) path = os.path.normpath(path) # import the max scene by using "merge file" path = path.replace('\\', '/') - rt.MergeMaxFile(path) + rt.MergeMaxFile(path, quiet=True) max_objects = rt.getLastMergedNodes() - max_container = rt.Container(name=f"{name}") - for max_object in max_objects: - max_object.Parent = max_container - + # implement the OP/AYON custom attributes before load + max_container = [] + container = rt.Container(name=name) + import_custom_attribute_data(container, max_objects) + max_container.append(container) + max_container.extend(max_objects) return containerise( - name, [max_container], context, loader=self.__class__.__name__) + name, max_container, context, loader=self.__class__.__name__) def update(self, container, representation): from pymxs import runtime as rt path = get_representation_path(representation) node_name = container["instance_node"] - - rt.MergeMaxFile(path, - rt.Name("noRedraw"), - rt.Name("deleteOldDups"), - rt.Name("useSceneMtlDups")) - + node = rt.GetNodeByName(node_name) + inst_name, _ = os.path.splitext(node_name) + old_container = rt.getNodeByName(inst_name) + # delete the old container with attribute + # delete old duplicate + rt.Delete(old_container) + rt.MergeMaxFile(path, rt.Name("deleteOldDups")) + new_container = rt.Container(name=inst_name) max_objects = rt.getLastMergedNodes() - container_node = rt.GetNodeByName(node_name) - for max_object in max_objects: - max_object.Parent = container_node + max_objects_list = [] + max_objects_list.append(new_container) + max_objects_list.extend(max_objects) + + for max_object in max_objects_list: + max_object.Parent = node + update_custom_attribute_data(new_container, max_objects) lib.imprint(container["instance_node"], { "representation": str(representation["_id"]) }) From 0825afa73a0f4a706d3b091cfb068565381d5a40 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 16 Aug 2023 14:22:14 +0800 Subject: [PATCH 114/327] add includedfullgroup support for merging scene in max scene family --- openpype/hosts/max/plugins/load/load_max_scene.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/load/load_max_scene.py b/openpype/hosts/max/plugins/load/load_max_scene.py index 637659ed44..7bbc6419b8 100644 --- a/openpype/hosts/max/plugins/load/load_max_scene.py +++ b/openpype/hosts/max/plugins/load/load_max_scene.py @@ -26,7 +26,7 @@ class MaxSceneLoader(load.LoaderPlugin): path = os.path.normpath(path) # import the max scene by using "merge file" path = path.replace('\\', '/') - rt.MergeMaxFile(path, quiet=True) + rt.MergeMaxFile(path, quiet=True, includeFullGroup=True) max_objects = rt.getLastMergedNodes() # implement the OP/AYON custom attributes before load max_container = [] From 86f86db4f84db54eecd127cbac528f3f9752107e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 16 Aug 2023 17:55:07 +0800 Subject: [PATCH 115/327] also resolves OP-6526_3dsMax-loading-an-asset-multiple-times --- openpype/hosts/max/api/pipeline.py | 4 ++-- openpype/hosts/max/plugins/load/load_max_scene.py | 6 +++++- openpype/hosts/max/plugins/load/load_model_fbx.py | 6 +----- openpype/hosts/max/plugins/load/load_model_obj.py | 8 +++----- openpype/hosts/max/plugins/load/load_model_usd.py | 1 + 5 files changed, 12 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/max/api/pipeline.py b/openpype/hosts/max/api/pipeline.py index 08ff5c6baf..f58bd05a13 100644 --- a/openpype/hosts/max/api/pipeline.py +++ b/openpype/hosts/max/api/pipeline.py @@ -188,7 +188,7 @@ def import_custom_attribute_data(container: str, selections: list): Args: container (str): target container which adds custom attributes - selections (_type_): nodes to be added into + selections (list): nodes to be added into group in custom attributes """ attrs = load_custom_attribute_data() @@ -216,7 +216,7 @@ def update_custom_attribute_data(container: str, selections: list): Args: container (str): target container which adds custom attributes - selections (_type_): nodes to be added into + selections (list): nodes to be added into group in custom attributes """ if container.modifiers[0].name == "OP Data": diff --git a/openpype/hosts/max/plugins/load/load_max_scene.py b/openpype/hosts/max/plugins/load/load_max_scene.py index 7bbc6419b8..2f5108aec5 100644 --- a/openpype/hosts/max/plugins/load/load_max_scene.py +++ b/openpype/hosts/max/plugins/load/load_max_scene.py @@ -51,7 +51,11 @@ class MaxSceneLoader(load.LoaderPlugin): rt.MergeMaxFile(path, rt.Name("deleteOldDups")) new_container = rt.Container(name=inst_name) max_objects = rt.getLastMergedNodes() - + current_max_objects = rt.getLastMergedNodes() + for current_object in current_max_objects: + prev_max_objects = prev_max_objects.remove(current_object) + for prev_object in prev_max_objects: + rt.Delete(prev_object) max_objects_list = [] max_objects_list.append(new_container) max_objects_list.extend(max_objects) diff --git a/openpype/hosts/max/plugins/load/load_model_fbx.py b/openpype/hosts/max/plugins/load/load_model_fbx.py index 26520307c9..d076bf2de9 100644 --- a/openpype/hosts/max/plugins/load/load_model_fbx.py +++ b/openpype/hosts/max/plugins/load/load_model_fbx.py @@ -26,11 +26,7 @@ class FbxModelLoader(load.LoaderPlugin): rt.FBXImporterSetParam("Preserveinstances", True) rt.importFile(filepath, rt.name("noPrompt"), using=rt.FBXIMP) - container = rt.GetNodeByName(name) - if not container: - container = rt.Container() - container.name = name - + container = rt.Container(name=name) selections = rt.GetCurrentSelection() import_custom_attribute_data(container, selections) diff --git a/openpype/hosts/max/plugins/load/load_model_obj.py b/openpype/hosts/max/plugins/load/load_model_obj.py index 05f37f9e5a..bac5b8b4f3 100644 --- a/openpype/hosts/max/plugins/load/load_model_obj.py +++ b/openpype/hosts/max/plugins/load/load_model_obj.py @@ -27,18 +27,16 @@ class ObjLoader(load.LoaderPlugin): rt.Execute(f'importFile @"{filepath}" #noPrompt using:ObjImp') # create "missing" container for obj import - container = rt.Container() - container.name = name + container = rt.Container(name=name) selections = rt.GetCurrentSelection() import_custom_attribute_data(container, selections) # get current selection for selection in selections: selection.Parent = container - - asset = rt.GetNodeByName(name) + self.log.debug(f"{container.ClassID}") return containerise( - name, [asset], context, loader=self.__class__.__name__) + name, [container], context, loader=self.__class__.__name__) def update(self, container, representation): from pymxs import runtime as rt diff --git a/openpype/hosts/max/plugins/load/load_model_usd.py b/openpype/hosts/max/plugins/load/load_model_usd.py index 425b152278..d3669fc10e 100644 --- a/openpype/hosts/max/plugins/load/load_model_usd.py +++ b/openpype/hosts/max/plugins/load/load_model_usd.py @@ -35,6 +35,7 @@ class ModelUSDLoader(load.LoaderPlugin): rt.USDImporter.importFile(filepath, importOptions=import_options) asset = rt.GetNodeByName(name) + import_custom_attribute_data(asset, asset.Children) return containerise( From ae42d524c80bab1d5f3583111eb5208c9d515caf Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 16 Aug 2023 18:22:59 +0800 Subject: [PATCH 116/327] fixing the error when updating the max scene in the loader --- openpype/hosts/max/plugins/load/load_max_scene.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_max_scene.py b/openpype/hosts/max/plugins/load/load_max_scene.py index 2f5108aec5..f73bb1941e 100644 --- a/openpype/hosts/max/plugins/load/load_max_scene.py +++ b/openpype/hosts/max/plugins/load/load_max_scene.py @@ -45,12 +45,12 @@ class MaxSceneLoader(load.LoaderPlugin): node = rt.GetNodeByName(node_name) inst_name, _ = os.path.splitext(node_name) old_container = rt.getNodeByName(inst_name) + prev_max_objects = rt.getLastMergedNodes() # delete the old container with attribute # delete old duplicate rt.Delete(old_container) rt.MergeMaxFile(path, rt.Name("deleteOldDups")) new_container = rt.Container(name=inst_name) - max_objects = rt.getLastMergedNodes() current_max_objects = rt.getLastMergedNodes() for current_object in current_max_objects: prev_max_objects = prev_max_objects.remove(current_object) @@ -58,11 +58,11 @@ class MaxSceneLoader(load.LoaderPlugin): rt.Delete(prev_object) max_objects_list = [] max_objects_list.append(new_container) - max_objects_list.extend(max_objects) + max_objects_list.extend(current_max_objects) for max_object in max_objects_list: max_object.Parent = node - update_custom_attribute_data(new_container, max_objects) + update_custom_attribute_data(new_container, current_max_objects) lib.imprint(container["instance_node"], { "representation": str(representation["_id"]) }) From 7b4a59e3338b1f3455e4362a876a3474454cf15b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 16 Aug 2023 13:42:02 +0200 Subject: [PATCH 117/327] nuke: adding inherited colorspace from instance --- openpype/hosts/nuke/plugins/publish/extract_thumbnail.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index 21eefda249..d57d55f85d 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -54,6 +54,7 @@ class ExtractThumbnail(publish.Extractor): def render_thumbnail(self, instance, output_name=None, **kwargs): first_frame = instance.data["frameStartHandle"] last_frame = instance.data["frameEndHandle"] + colorspace = instance.data["colorspace"] # find frame range and define middle thumb frame mid_frame = int((last_frame - first_frame) / 2) @@ -112,8 +113,8 @@ class ExtractThumbnail(publish.Extractor): if self.use_rendered and os.path.isfile(path_render): # check if file exist otherwise connect to write node rnode = nuke.createNode("Read") - rnode["file"].setValue(path_render) + rnode["colorspace"].setValue(colorspace) # turn it raw if none of baking is ON if all([ From a081f5aed5239bbc88703904a83929530877f031 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 16 Aug 2023 15:26:16 +0300 Subject: [PATCH 118/327] add_kitsu_credentials --- .../modules/deadline/plugins/publish/submit_publish_job.py | 4 +++- 1 file changed, 3 insertions(+), 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 5e8c005d07..76cb357f91 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -121,7 +121,9 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, "FTRACK_SERVER", "AVALON_APP_NAME", "OPENPYPE_USERNAME", - "OPENPYPE_SG_USER" + "OPENPYPE_SG_USER", + "KITSU_LOGIN", + "KITSU_PWD" ] # custom deadline attributes From 8aa150cfe5af1c7259b5f4466835638249c29b73 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 16 Aug 2023 21:37:49 +0800 Subject: [PATCH 119/327] fixing the bug of not being able to update the scene when using maxSceneloader and some clean up --- .../hosts/max/plugins/load/load_camera_fbx.py | 13 ++++---- .../hosts/max/plugins/load/load_max_scene.py | 33 ++++++++++--------- openpype/hosts/max/plugins/load/load_model.py | 6 ++-- .../hosts/max/plugins/load/load_model_fbx.py | 14 +++++--- .../hosts/max/plugins/load/load_model_obj.py | 7 ++-- .../hosts/max/plugins/load/load_pointcache.py | 14 ++++---- 6 files changed, 46 insertions(+), 41 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_camera_fbx.py b/openpype/hosts/max/plugins/load/load_camera_fbx.py index 1e4e5b3e91..87745ae881 100644 --- a/openpype/hosts/max/plugins/load/load_camera_fbx.py +++ b/openpype/hosts/max/plugins/load/load_camera_fbx.py @@ -45,12 +45,11 @@ class FbxLoader(load.LoaderPlugin): from pymxs import runtime as rt path = get_representation_path(representation) - node = rt.GetNodeByName(container["instance_node"]) - inst_name, _ = os.path.split(container["instance_node"]) - container = rt.getNodeByName(inst_name) + node_name = container["instance_node"] + node = rt.getNodeByName(node_name) + inst_name, _ = node_name.split("_") rt.Select(node.Children) - update_custom_attribute_data( - container, rt.GetCurrentSelection()) + rt.FBXImporterSetParam("Animation", True) rt.FBXImporterSetParam("Camera", True) rt.FBXImporterSetParam("Mode", rt.Name("merge")) @@ -58,7 +57,9 @@ class FbxLoader(load.LoaderPlugin): rt.FBXImporterSetParam("Preserveinstances", True) rt.ImportFile( path, rt.name("noPrompt"), using=rt.FBXIMP) - + inst_container = rt.getNodeByName(inst_name) + update_custom_attribute_data( + inst_container, rt.GetCurrentSelection()) with maintained_selection(): rt.Select(node) diff --git a/openpype/hosts/max/plugins/load/load_max_scene.py b/openpype/hosts/max/plugins/load/load_max_scene.py index f73bb1941e..348b940b22 100644 --- a/openpype/hosts/max/plugins/load/load_max_scene.py +++ b/openpype/hosts/max/plugins/load/load_max_scene.py @@ -42,27 +42,28 @@ class MaxSceneLoader(load.LoaderPlugin): path = get_representation_path(representation) node_name = container["instance_node"] - node = rt.GetNodeByName(node_name) - inst_name, _ = os.path.splitext(node_name) - old_container = rt.getNodeByName(inst_name) - prev_max_objects = rt.getLastMergedNodes() + node = rt.getNodeByName(node_name) + inst_name, _ = node_name.split("_") + inst_container = rt.getNodeByName(inst_name) # delete the old container with attribute # delete old duplicate - rt.Delete(old_container) + prev_max_object_names = [obj.name for obj in rt.getLastMergedNodes()] rt.MergeMaxFile(path, rt.Name("deleteOldDups")) - new_container = rt.Container(name=inst_name) - current_max_objects = rt.getLastMergedNodes() - for current_object in current_max_objects: - prev_max_objects = prev_max_objects.remove(current_object) - for prev_object in prev_max_objects: - rt.Delete(prev_object) - max_objects_list = [] - max_objects_list.append(new_container) - max_objects_list.extend(current_max_objects) - for max_object in max_objects_list: + current_max_objects = rt.getLastMergedNodes() + current_max_object_names = [obj.name for obj in rt.getLastMergedNodes()] + for obj in current_max_object_names: + idx = rt.findItem(prev_max_object_names, obj) + if idx: + prev_max_object_names = rt.deleteItem(prev_max_object_names, idx) + for object_name in prev_max_object_names: + prev_max_object = rt.getNodeByName(object_name) + rt.Delete(prev_max_object) + + update_custom_attribute_data(inst_container, current_max_objects) + + for max_object in current_max_objects: max_object.Parent = node - update_custom_attribute_data(new_container, current_max_objects) lib.imprint(container["instance_node"], { "representation": str(representation["_id"]) }) diff --git a/openpype/hosts/max/plugins/load/load_model.py b/openpype/hosts/max/plugins/load/load_model.py index f71e4e8f7f..a84d497aab 100644 --- a/openpype/hosts/max/plugins/load/load_model.py +++ b/openpype/hosts/max/plugins/load/load_model.py @@ -70,9 +70,9 @@ class ModelAbcLoader(load.LoaderPlugin): update_custom_attribute_data(abc, abc.Children) rt.Select(abc.Children) for abc_con in rt.Selection: - container = rt.GetNodeByName(abc_con.name) - container.source = path - rt.Select(container.Children) + abc_container = rt.GetNodeByName(abc_con.name) + abc_container.source = path + rt.Select(abc_container.Children) for abc_obj in rt.Selection: alembic_obj = rt.GetNodeByName(abc_obj.name) alembic_obj.source = path diff --git a/openpype/hosts/max/plugins/load/load_model_fbx.py b/openpype/hosts/max/plugins/load/load_model_fbx.py index d076bf2de9..f7d3dab60c 100644 --- a/openpype/hosts/max/plugins/load/load_model_fbx.py +++ b/openpype/hosts/max/plugins/load/load_model_fbx.py @@ -26,7 +26,10 @@ class FbxModelLoader(load.LoaderPlugin): rt.FBXImporterSetParam("Preserveinstances", True) rt.importFile(filepath, rt.name("noPrompt"), using=rt.FBXIMP) + container = rt.GetNodeByName(name) + container = rt.Container(name=name) + selections = rt.GetCurrentSelection() import_custom_attribute_data(container, selections) @@ -40,8 +43,9 @@ class FbxModelLoader(load.LoaderPlugin): def update(self, container, representation): from pymxs import runtime as rt path = get_representation_path(representation) - node = rt.getNodeByName(container["instance_node"]) - inst_name, _ = os.path.splitext(container["instance_node"]) + node_name = container["instance_node"] + node = rt.getNodeByName(node_name) + inst_name, _ = node_name.split("_") rt.select(node.Children) rt.FBXImporterSetParam("Animation", False) @@ -52,14 +56,14 @@ class FbxModelLoader(load.LoaderPlugin): rt.FBXImporterSetParam("Preserveinstances", True) rt.importFile(path, rt.name("noPrompt"), using=rt.FBXIMP) - container = rt.getNodeByName(inst_name) + inst_container = rt.getNodeByName(inst_name) update_custom_attribute_data( - container, rt.GetCurrentSelection()) + inst_container, rt.GetCurrentSelection()) with maintained_selection(): rt.Select(node) lib.imprint( - container["instance_node"], + node_name, {"representation": str(representation["_id"])}, ) diff --git a/openpype/hosts/max/plugins/load/load_model_obj.py b/openpype/hosts/max/plugins/load/load_model_obj.py index bac5b8b4f3..9979ca36b0 100644 --- a/openpype/hosts/max/plugins/load/load_model_obj.py +++ b/openpype/hosts/max/plugins/load/load_model_obj.py @@ -33,7 +33,6 @@ class ObjLoader(load.LoaderPlugin): # get current selection for selection in selections: selection.Parent = container - self.log.debug(f"{container.ClassID}") return containerise( name, [container], context, loader=self.__class__.__name__) @@ -46,7 +45,7 @@ class ObjLoader(load.LoaderPlugin): node = rt.GetNodeByName(node_name) instance_name, _ = node_name.split("_") - container = rt.GetNodeByName(instance_name) + inst_container = rt.GetNodeByName(instance_name) for child in container.Children: rt.Delete(child) @@ -54,8 +53,8 @@ class ObjLoader(load.LoaderPlugin): # get current selection selections = rt.GetCurrentSelection() for selection in selections: - selection.Parent = container - update_custom_attribute_data(container, selections) + selection.Parent = inst_container + update_custom_attribute_data(inst_container, selections) with maintained_selection(): rt.Select(node) diff --git a/openpype/hosts/max/plugins/load/load_pointcache.py b/openpype/hosts/max/plugins/load/load_pointcache.py index 0ec9fda3d5..953141c4ac 100644 --- a/openpype/hosts/max/plugins/load/load_pointcache.py +++ b/openpype/hosts/max/plugins/load/load_pointcache.py @@ -70,10 +70,6 @@ class AbcLoader(load.LoaderPlugin): path = get_representation_path(representation) node = rt.GetNodeByName(container["instance_node"]) - lib.imprint( - container["instance_node"], - {"representation": str(representation["_id"])}, - ) nodes_list = [] with maintained_selection(): rt.Select(node.Children) @@ -83,14 +79,18 @@ class AbcLoader(load.LoaderPlugin): update_custom_attribute_data(abc, abc.Children) rt.Select(abc.Children) for abc_con in rt.Selection: - container = rt.GetNodeByName(abc_con.name) - container.source = path - rt.Select(container.Children) + abc_container = rt.GetNodeByName(abc_con.name) + abc_container.source = path + rt.Select(abc_container.Children) for abc_obj in rt.Selection: alembic_obj = rt.GetNodeByName(abc_obj.name) alembic_obj.source = path nodes_list.append(alembic_obj) + lib.imprint( + container["instance_node"], + {"representation": str(representation["_id"])}, + ) def switch(self, container, representation): self.update(container, representation) From 328c3d9c7fa499ca39b1351b94f2d0ae0d261a69 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 16 Aug 2023 16:16:21 +0200 Subject: [PATCH 120/327] OP-6567 - fix setting of version to workfile instance (#5452) If there are multiple instances of renderlayer published, previous logic resulted in unpredictable rewrite of instance family to 'workfile' --- openpype/hosts/maya/plugins/publish/collect_render.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index c37b54ea9a..c17a8789e4 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -304,9 +304,9 @@ class CollectMayaRender(pyblish.api.InstancePlugin): if self.sync_workfile_version: data["version"] = context.data["version"] - for instance in context: - if instance.data['family'] == "workfile": - instance.data["version"] = context.data["version"] + for _instance in context: + if _instance.data['family'] == "workfile": + _instance.data["version"] = context.data["version"] # Define nice label label = "{0} ({1})".format(layer_name, instance.data["asset"]) From 3342ceff2cee9a44c34c265cb51c7e2e8bcfa799 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 16 Aug 2023 22:39:36 +0800 Subject: [PATCH 121/327] clean up on the fbx and max_scene code --- openpype/hosts/max/plugins/load/load_camera_fbx.py | 4 ++++ openpype/hosts/max/plugins/load/load_max_scene.py | 10 ++++++---- openpype/hosts/max/plugins/load/load_model_fbx.py | 11 ++++++----- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_camera_fbx.py b/openpype/hosts/max/plugins/load/load_camera_fbx.py index 87745ae881..86e201afa8 100644 --- a/openpype/hosts/max/plugins/load/load_camera_fbx.py +++ b/openpype/hosts/max/plugins/load/load_camera_fbx.py @@ -57,7 +57,11 @@ class FbxLoader(load.LoaderPlugin): rt.FBXImporterSetParam("Preserveinstances", True) rt.ImportFile( path, rt.name("noPrompt"), using=rt.FBXIMP) + current_fbx_objects = rt.GetCurrentSelection() inst_container = rt.getNodeByName(inst_name) + for fbx_object in current_fbx_objects: + if fbx_object.Parent != inst_container: + fbx_object.Parent = inst_container update_custom_attribute_data( inst_container, rt.GetCurrentSelection()) with maintained_selection(): diff --git a/openpype/hosts/max/plugins/load/load_max_scene.py b/openpype/hosts/max/plugins/load/load_max_scene.py index 348b940b22..4f29f6bd3a 100644 --- a/openpype/hosts/max/plugins/load/load_max_scene.py +++ b/openpype/hosts/max/plugins/load/load_max_scene.py @@ -47,13 +47,15 @@ class MaxSceneLoader(load.LoaderPlugin): inst_container = rt.getNodeByName(inst_name) # delete the old container with attribute # delete old duplicate - prev_max_object_names = [obj.name for obj in rt.getLastMergedNodes()] + prev_max_object_names = [obj.name for obj + in rt.getLastMergedNodes()] rt.MergeMaxFile(path, rt.Name("deleteOldDups")) current_max_objects = rt.getLastMergedNodes() - current_max_object_names = [obj.name for obj in rt.getLastMergedNodes()] - for obj in current_max_object_names: - idx = rt.findItem(prev_max_object_names, obj) + current_max_object_names = [obj.name for obj + in current_max_objects] + for name in current_max_object_names: + idx = rt.findItem(prev_max_object_names, name) if idx: prev_max_object_names = rt.deleteItem(prev_max_object_names, idx) for object_name in prev_max_object_names: diff --git a/openpype/hosts/max/plugins/load/load_model_fbx.py b/openpype/hosts/max/plugins/load/load_model_fbx.py index f7d3dab60c..67252a73ff 100644 --- a/openpype/hosts/max/plugins/load/load_model_fbx.py +++ b/openpype/hosts/max/plugins/load/load_model_fbx.py @@ -46,19 +46,20 @@ class FbxModelLoader(load.LoaderPlugin): node_name = container["instance_node"] node = rt.getNodeByName(node_name) inst_name, _ = node_name.split("_") - rt.select(node.Children) + inst_container = rt.getNodeByName(inst_name) rt.FBXImporterSetParam("Animation", False) rt.FBXImporterSetParam("Cameras", False) rt.FBXImporterSetParam("Mode", rt.Name("merge")) rt.FBXImporterSetParam("AxisConversionMethod", True) - rt.FBXImporterSetParam("UpAxis", "Y") rt.FBXImporterSetParam("Preserveinstances", True) rt.importFile(path, rt.name("noPrompt"), using=rt.FBXIMP) - - inst_container = rt.getNodeByName(inst_name) + current_fbx_objects = rt.GetCurrentSelection() + for fbx_object in current_fbx_objects: + if fbx_object.Parent != inst_container: + fbx_object.Parent = inst_container update_custom_attribute_data( - inst_container, rt.GetCurrentSelection()) + inst_container, current_fbx_objects) with maintained_selection(): rt.Select(node) From 8345298913cf88205b1217c261ad3c0dcdf6a946 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 16 Aug 2023 22:40:56 +0800 Subject: [PATCH 122/327] hound --- openpype/hosts/max/plugins/load/load_max_scene.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/load/load_max_scene.py b/openpype/hosts/max/plugins/load/load_max_scene.py index 4f29f6bd3a..9c7468b8fc 100644 --- a/openpype/hosts/max/plugins/load/load_max_scene.py +++ b/openpype/hosts/max/plugins/load/load_max_scene.py @@ -57,7 +57,8 @@ class MaxSceneLoader(load.LoaderPlugin): for name in current_max_object_names: idx = rt.findItem(prev_max_object_names, name) if idx: - prev_max_object_names = rt.deleteItem(prev_max_object_names, idx) + prev_max_object_names = rt.deleteItem( + prev_max_object_names, idx) for object_name in prev_max_object_names: prev_max_object = rt.getNodeByName(object_name) rt.Delete(prev_max_object) From c5d882c7eae662deb1a6477bb93fe7884f033dca Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 17 Aug 2023 10:33:52 +0200 Subject: [PATCH 123/327] Maya: Fix wrong subset name of render family in deadline (#5442) * Use existing subset_name as group_name by default New publisher already carries real subset name (`renderModelingMain`), it should build group name only if subset_name is weird. * Let legacy conversion of render instance recreate subset_name Without it would create subset names like `renderingMain` which are not matching to newly created `renderMain` instances. This would cause issue in version restarts. * Let Render Creator for Maya create proper subset_name It was using hardcoded logic not matching other DCCs. * Hound * Fix method calls * Fix typos * Do not import unnecessary * Capitalize is wrong function for here * Overwrite get_subset_name for standardized results It makes sense to override this method for other parts of code getting same results. * Force change It seems that GH doesn't recognize changes with adding() * Update openpype/hosts/maya/plugins/create/convert_legacy.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * Hound --------- Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/maya/api/plugin.py | 37 +++++++++++++++---- .../maya/plugins/create/convert_legacy.py | 14 +++++++ openpype/pipeline/farm/pyblish_functions.py | 12 ++++-- 3 files changed, 53 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index f705133e4f..00d6602ef9 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -22,10 +22,10 @@ from openpype.pipeline import ( LegacyCreator, LoaderPlugin, get_representation_path, - - legacy_io, ) from openpype.pipeline.load import LoadError +from openpype.client import get_asset_by_name +from openpype.pipeline.create import get_subset_name from . import lib from .lib import imprint, read @@ -405,14 +405,21 @@ class RenderlayerCreator(NewCreator, MayaCreatorBase): # No existing scene instance node for this layer. Note that # this instance will not have the `instance_node` data yet # until it's been saved/persisted at least once. - # TODO: Correctly define the subset name using templates - prefix = self.layer_instance_prefix or self.family - subset_name = "{}{}".format(prefix, layer.name()) + project_name = self.create_context.get_current_project_name() + instance_data = { - "asset": legacy_io.Session["AVALON_ASSET"], - "task": legacy_io.Session["AVALON_TASK"], + "asset": self.create_context.get_current_asset_name(), + "task": self.create_context.get_current_task_name(), "variant": layer.name(), } + asset_doc = get_asset_by_name(project_name, + instance_data["asset"]) + subset_name = self.get_subset_name( + layer.name(), + instance_data["task"], + asset_doc, + project_name) + instance = CreatedInstance( family=self.family, subset_name=subset_name, @@ -519,6 +526,22 @@ class RenderlayerCreator(NewCreator, MayaCreatorBase): if node and cmds.objExists(node): cmds.delete(node) + def get_subset_name( + self, + variant, + task_name, + asset_doc, + project_name, + host_name=None, + instance=None + ): + # creator.family != 'render' as expected + return get_subset_name(self.layer_instance_prefix, + variant, + task_name, + asset_doc, + project_name) + class Loader(LoaderPlugin): hosts = ["maya"] diff --git a/openpype/hosts/maya/plugins/create/convert_legacy.py b/openpype/hosts/maya/plugins/create/convert_legacy.py index 33a1e020dd..cd8faf291b 100644 --- a/openpype/hosts/maya/plugins/create/convert_legacy.py +++ b/openpype/hosts/maya/plugins/create/convert_legacy.py @@ -2,6 +2,8 @@ from openpype.pipeline.create.creator_plugins import SubsetConvertorPlugin from openpype.hosts.maya.api import plugin from openpype.hosts.maya.api.lib import read +from openpype.client import get_asset_by_name + from maya import cmds from maya.app.renderSetup.model import renderSetup @@ -135,6 +137,18 @@ class MayaLegacyConvertor(SubsetConvertorPlugin, # "rendering" family being converted to "renderlayer" family) original_data["family"] = creator.family + # recreate subset name as without it would be + # `renderingMain` vs correct `renderMain` + project_name = self.create_context.get_current_project_name() + asset_doc = get_asset_by_name(project_name, + original_data["asset"]) + subset_name = creator.get_subset_name( + original_data["variant"], + data["task"], + asset_doc, + project_name) + original_data["subset"] = subset_name + # Convert to creator attributes when relevant creator_attributes = {} for key in list(original_data.keys()): diff --git a/openpype/pipeline/farm/pyblish_functions.py b/openpype/pipeline/farm/pyblish_functions.py index 8b9058359e..288602b77c 100644 --- a/openpype/pipeline/farm/pyblish_functions.py +++ b/openpype/pipeline/farm/pyblish_functions.py @@ -568,9 +568,15 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data, col = list(cols[0]) # create subset name `familyTaskSubset_AOV` - group_name = 'render{}{}{}{}'.format( - task[0].upper(), task[1:], - subset[0].upper(), subset[1:]) + # TODO refactor/remove me + family = skeleton["family"] + if not subset.startswith(family): + group_name = '{}{}{}{}{}'.format( + family, + task[0].upper(), task[1:], + subset[0].upper(), subset[1:]) + else: + group_name = subset # if there are multiple cameras, we need to add camera name if isinstance(col, (list, tuple)): From 447921b22e51f0fbc412dcbae72ab543c60a93a9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 17 Aug 2023 10:38:21 +0200 Subject: [PATCH 124/327] Publisher: Thumbnail widget enhancements (#5439) * screenshot widget from @BigRoy * small tweaks of screen capture logic * added take screenshot button to thumbnail widget * added tooltips * Use constants from class * adde PySide 6 support * minimize window when on take screenshot * Keep origin state of window. Co-authored-by: Roy Nieterau * Fix support for Qt version below 5.10 * draw pixel with alpha when disabled * clear image cache on resize * added more buttons and options button with animation * removed unnecessary options widget * fix escape button * keep icons visible all the time --------- Co-authored-by: Roy Nieterau --- .../tools/publisher/widgets/images/browse.png | Bin 0 -> 12225 bytes .../publisher/widgets/images/options.png | Bin 0 -> 3216 bytes .../tools/publisher/widgets/images/paste.png | Bin 0 -> 6513 bytes .../widgets/images/take_screenshot.png | Bin 0 -> 11003 bytes .../publisher/widgets/screenshot_widget.py | 314 ++++++++++++++++++ .../publisher/widgets/thumbnail_widget.py | 126 ++++++- openpype/tools/utils/widgets.py | 18 + 7 files changed, 449 insertions(+), 9 deletions(-) create mode 100644 openpype/tools/publisher/widgets/images/browse.png create mode 100644 openpype/tools/publisher/widgets/images/options.png create mode 100644 openpype/tools/publisher/widgets/images/paste.png create mode 100644 openpype/tools/publisher/widgets/images/take_screenshot.png create mode 100644 openpype/tools/publisher/widgets/screenshot_widget.py diff --git a/openpype/tools/publisher/widgets/images/browse.png b/openpype/tools/publisher/widgets/images/browse.png new file mode 100644 index 0000000000000000000000000000000000000000..b115bb67662ead09af54700e8b7f91b580672916 GIT binary patch literal 12225 zcmdsdi93{S8}~gk4B3@zW#1FBBs+x|WeHis6hg9P%{rrGE6Z4lY-NzLm3?e?Pdi0q z%aD0e*_pCt{jSmTyzhH_|H1bi2gcm@c`fI8{?7Hf5-rS(Sef{lAP8c`7#mnZ5EA^0 zgc#A_V>zsU1A-8EFMWNBKue=j!Wct+RYi4GRXHVjMF^5k4^D46gF49(-(m3TQF?-d zogE#fVt+5=$wSEr3Kq%YvFGl8`?dd<+JdNPLu_S{@W}d+WrdG6HQaQDoG)#>4G*zS zUJw;e`0ReUeS5yUMw5Ba|Ma_=mF=_-2kR&Xu)2`FziK~GAz8lD7RH?w47KR@VD~CR7oDLcI6ej92Zlw ztlwlexxA zqF9pGCSDm8Ry;}{cj+vYP-spH%Q~)-jY%)X8TB75FG#z`r7fcTBKPxdN7O*ZjrME7 z5fFa?>%9bOQQdoWqBI8R#3GtbvJPfrlX{QvM%XYJZcPv^io%r$(q&WEu= zdBnzf&Vy%H+jOmCb3?QJ^rvlDpm<)A;y@V!AK2&ncT&VzolOx~0jw?d1jQa!g)2TL zam^zr{1i*dC5kR?cVDG3p+D(3{smr$kh088YBqi5#L{{&Ooc%Fs>Q07sIq-XyQnFv zS(`DBWiC$HkdWD?jjv4*(i**0zl~PgZzq7Uv?-v=JGrIzx9Z${2*nf2Dvo(Dh|t-m zb!v-3vFQns^_LEodzeMMI>)f3@bd=Vv5njeXQYd-Tslrkei^#s>&uLf$9KG0K@j@O zP4Th`i_2r9+~ZZK5CZ~}q9zIF6~;ClzGE6Yo0je7L&+i@^WT{&BoO(+1&$T27i*np zEWcXzcNUVMBIXLWBrJX$8!c_wMus5?#P8RR{uF4wPyXfGm{Fi9g2}zrA=!=!4ZJ_S zdd#1ZkePFV2O)@#)F3gI)7Rp72$xavY*mJY9q~6%%?W=d9h~6{#nYsKfkj0+<4yx# z1olARi%1*hD*c(i+Tj%dN))`e4ixM2J^rP%s><9(bHZZ}chz*7662bA*qxyuO0 zx;Z7FH_rI6(Ki-&0`HBvyNii+LAD@54R}Po)LNkw4W(HK4<3qee1KRt>SKQ=;OmXU2`b zy25f{FAjK-7SJK+=z~rP#EVm!P=z@YL?~Tsgj3*Muwr>+R3Y*nIWVFx?J7&8kYAx( zIXxOGKWfg*ZUvvJ)Lx`9IPB)3yRs>Lt`-z+Qc<#;GIOVNHRcEnyRg*9mS>&Pg z0xfm7K-?nKLsvyZL-x!OOR4R=4i{jv2FL+mX-1yV?-omHyjeZA~GR-k@(-EB5 zeM52zx8?Qnn;F7H{3C-z1T@kvcIaZ_NZIRR#CHu{nh|TAuXw5ZzUq2A`TF_HH%egS zno`zN0-nu3>;BSqO^JDZ|Ifn4ss?%(QMuh9?sY3#Kk|**=MXQCMdxOF#Cqsg-9RUw zHKb$XDw&`nU4`&;;B~@32XwoiTZ9jyqn@E$CwfoEUv}#Ij`s$5lSxsAOwo|s zHASzVqg!BGH3xygt~xxXBP~93F^qs9-4XA?-B3f7l;+NR$fx%8cHmaSdZFp+c+}2qAM<&SAL$FPuS-pHj}4_XxTNkx zB&d?fnj&lsD( zp5G)b2~(LZIdpl{@-Kd=ueFMbNnD_}nQi_sy@8nKt~qXZAWW%7v8H#b@DMUpa!fKr zf^^7u9+s&89^EH8*n{$zxU?WM-EyFv-L8I8wQTn6tq9ZQ*F^fOnh-g|mtj`#sF!gS zN{1mbF=$C$&Tu?`R|^Y_FRx13_{KMm+X!=dHachM>H)_T@s%GJ`?lqDj2%uCy?Q^> z+_pbOHU3y|oji4jI6I)gpo6>7K_pc$w&wM-M;EA|9tUi&K~as3S|;sGltiLe^?m#V zUhYy?I?=xtFJ@c|Ei(LgnVrGz0p*&oK#q`$h!ZJTEgF{a3J5f=eQ-B@@Z|X(AaF}) zIBgl1yNhNkxy{FoNm_QJc)#5ZB}1dMD(<37VJxMZ9Z{g5a|Uy)&5W%>&u zYFtn^vj^$fsR(J|gQ{XCLAdZLN`VB8!e77P5LzDiILV^7?}qNTP$AqTboVPo0pmn2 z43hmjxR`nR2BJvjE+5V?*}9uZ_?$h0^&NvSG?u@ zb-dCkSUxECAqqZ9v4YP%4>q>^AZV^3P;)Lw#7PlSF+}4I8aMum(#NtAh;9cc@hra(Y~c*w}zMA@gpVvG+4{Ne^Vvd8r|x3mU&U zxg{av;Ya5z79FK(bME zxI1{)2u~wI|D6s7K>??iXMrv@7@=Lpiavc!0@1eu6)8?i$T9CExq z%x|_g`*yBA)>UR6PUw%xew+|D4%k*j!Dz0q7|-)1m1deV9@nb<7#A;k#guhTmR1;& zI(9Ur11m>1;BeTdH|}Wi4J)hrLNhClk(G>%89Hz(2U+C%pa|RF9;5}hB&X?+8q+#A zRiF>+klV})?GQ8haH?6nkS{|D!wzecdJ{J$xFv0wqh2UYu*r(h4FB#mJer&g-5=39 z;!yT#Xy%0n&J>>tj6sHd2+@LQG~iJ(4J>B9cN9B!R4jyf;%OxOL~*O|_$0bWQxRbG!!N@0dy^%|b- zCH2yuea(mS&DswMCemJDb^L>ZIIZgBb5bLKMEE@*7KmW-)0OgiO0*QqR$~dkbP4!# z$k?)-8|p}qzm)V$=Y#K`Ctsq4oM0|Kh{+X7BerH;7V5``20?sF38Cv&9H@D;t{-q(GQz0kS_>Z2SWt_Q9Q z2|~6qM94gW(qjf@fu_xt3^@XO`oD&kD?;!0SbdNQrLEwrm7%dCBGA#IylE8-m_!x~ zuKxh3Z7YA^YmuE>e>)Z5>A$gzzlN#RDR_1NK(= zAP2DOm8BcUTY$Eb5PFtgERY-k-*qNE!ml5q@c&E-J#+TB8ys*F$P%o9)Ei`Ik7Bd1 z-xbxk1X)bm&<*nXJW1V|)1egfu3A_)J^n{P9*}i%Jx&fMXSAEge3ecMZI_`kRju;Q zTg!k68AIob@};XnR0%eY4V;|+cMJ-5%QJYd_~C=CaBmu@q(GceYJ(J{dGv1&UsN>x z1(rO9t(#oD#P8=jMd1qb+C#Kthd(w8pO4c%O{!Gk?6~Kie*dblIcD6`{7dSxci3cKA zf*yIh62-P(;BMgS0jaz!is5!c1kMjBh0ra{D|cS$`d6n%p1b12aJGp1^h156$8j%= z$fj^2?14#UE-^IOcgsFC;tS&=kmuz2&_yEG<*1yuj;|~D>Ql7g75Fa3R@T^}n?-Lh z8DgcO;=5-cfOs%`AM@!)y&WQqo;nVf!>UieOQTBYa20RbDn4O2P`GQ)_^5uw*=ZL; zlA?L#Yg`8;kJ>>Et2P{_SAyzbKHS3Xi_phkjsok_{u2I-w^m-w>yt961|!Xd_)cI3 z0MO-?>^3q24G3WcSNJ3mzbieX^R+PPg>kKzU+HbQGNg&I24KCk0jVdGpNT1~nl@ewr#~$X6-Gee>7W_EnX*Iie;}g_2HYkN!R) z0l8@}xaXV2@|dN>ULlER`F$^zxScZLrVBfdRxGN+Rl zuVj5xxmXT%N;5eRsK5R*tO(l^QMENzc^%Ij4WN&J2N+CsrW=yq5mot;PA^nQ^AY%;m#Jh@TME zpk!0N{09t*kY~C!Cbc2ztiOOs)E5styM;@kiufP%%R^Y)XOkRjj9!^KrSg0ebFwIN zv0U;RC-#q-*MC06X}kV!Eagn*Qgih{GF~C$x>4mV znvIRe=q>{&eCRJA8m(X6{ytQ=B|!}sb&*i~qgq~QW;uwWl4m3T;XZEdG|vN|gE|=c z_>BdZJ@fY=er;9E+^YmslD4vJ0FYKX8x+({~N22SqT$|a@4$y*mJ;M3>KMM>eK2u24!Y@ZOtC2WNEli-d;fH}g8{mda zev5wpF;7HPsVrgc9w{&gKmEL|Yv-reeTKXfBRYCabUg?*dbXlgvS>A9dHMcN@>3Gd-aQ$+aYfcOLL>$98 z;g?*yatNP^Zy4?>)t(3ftJ%jm1J_@G{Mc7vrjgYBS&QP?*E_TJHfFbgU5i=m_VDM# z@?k`nX%Hv!*X{)c!d}>V?1L`KEU!D<$}@b7l;EzGuQITEj`D!wQ0gk;b?z%S@sq5& zW=dW5$)_MZNPm~EH?OD1S+*PZs)|)^_4K|V(nm8uy9X|GogJlb6c}>$})4h_L#Qv z@un%vaO7~9eGumve?e-{t?L=Nj>c>3>vlmRXO1z4g$8kE$x6&!)@B2N57eQr1Wcn{ zzJ0KFYU7WZ|aq2a+uK1(!#^T(=VN7}x+pO2~H+QwBeZ%XuU;R7;pZvDc&W=(|lz(Um zY+eoZG2ilwpNtEUF%{tq5?Zw~gkRB@s1ObRuprKf3OxLn&Uq3rr zV79|9loX^){RI|v=5*3?H&OS0w;@ahg%_EhruwLn!u2?(CESUxJ2~&|;ly4;D4wvu zS`?l6tL<(0sPbM8p+360b|Z5eTc{(jZBeV*47B8h9x_Lqty7}5 zQuwRr%owX4h<8Yh)yRzCd+DuTLLXx-OUpOE$>2!ybQRneUTD6XZD^EOSMvArXN_QJ zbjh$}yTAg8(NYkL&duY{**YWtO>M9`Xldt??$*`66L~vHAb2na@lt^ z28CUMSG~`5meUXp5-=;1rX$~z`{O<68luqtj@V88{d^GA!0g+BE;-!fwg>Ax#G@fA zBw0tMX8u8Q_AmpzVRXusz5USeqy+U(pMza*j6~C|rxrEDAGxMj!j7JC-qSl^sJ!6` zq(z^0m2-I25N^ZIv-f_+`xr%MU%3c*o|L&^T?nSPax2>|h8yB%?!K%n4-T4v_|4c6L6R(roU)-c@Ylb&Qm})G9Mk=452fK1QXPFGt32Y&3vU1&5U

e`IbY(RD9$;aj(&CdK~IMa6o?{f$ut-L7;v1Pr|$i1hi z*C4h3;|0Vq4#fX(FN50{6&h}^dNr2ha7P#L>X1oWY5|z77QIE`TdALX5b5B`=1RX= zBk-#K2~xo_&uUenpf2dx**`eQR5BftLg_^b^80WOvVm34b-1AZ;l~wJCT4@0(Rr4H zprMBvb01=XJb9@viyDfnbRHSfvi$&1aeQOZ%Li}eXm2;dn6QujM;IDo-2GLEB27nh zLzFj`e0)#Lw3vCF51a4HawXIiTMxkH`vh(%)-kV_{@(8nyb368%4tDE!E&f--_?js z0FbMba6>iI)s}zY`(NK!k?Dv0-CAKoB;({i12FSO?|H5W%j3qC4Xc|^ZZwsA&?eSjTw%1&L6}@ z-53#k%tPNW|9f}O?zIo(fszHtyV?b?=ZPij*Q(c?!EUA(7Vk|FB|P=QvqsY~UGTToO zSYm^K4S7=_b}7-RZ5wUL@$r{tT2Nq#^lsvKxz2D$11%}z$AyofwzO)tn_tR1%Z~9< z+bR6rY`!kn-;rYE+|y>Pe3YRAM_3cra;@_O;^6Wsk9-jCw8y=hry0rC;K2e%pPy&i!z^JgtlKRm43bgMR&T#fOW zz(~=YZa(tu28y48puDH>H_&|%yr7Q}L`5DYF;eQLkVh{4!!iOs4w<^7%Hve>w>^3d zxxg8K#`EK&EO|T^^i55sU09*GpIo?hbJB5t;(2^bX|NE>WzbVzM^n-ZuVx(v2jzUVg-o_9>9T%JQzsgk*xhD?sCXFL9v zDIt_45TFBvsa`Xi@^a6FuA4UMo+5c=Y}j^&3eV~cf>8MhiRKiovWFu1hjbC^g6_I{Q|DkQyt1+R zdROll98sKX!MW|d`cEEun0b`T>7~*f!-ai^S>Nxga@Tl`dtq!e;Kx+uu4IG)z4iw) zuQSYOww#N2yYV9DK?EK=R7^Z?-1x2ysfOFqjc-X4ERGPZarT$X0IBc3(y!llnw`aG zbsBCK_qV&Zy>PJk6yej=h_UK#ChuRDpT4kh##nYHQ1P@zPmCb-fFDEpMhM8XIkX_dZ?2SKTEOKQCb|{@yrEE-zE=!f%mO|q= zL+{=CszLDsZNAcGVqEMA)HnkhdT{E&*m=y_(RE3xCPf+k6ye$EGm9$A_>%fny`ZkB zhkRwQ;RP^S2OTMB6N;doAthP~?AJeEC95Nsw~Oi747 zCs@09KDD^eK0_s<20h)^_5mk%n*3UOK+s)*GLFB2i;wj#&zZn5Ukk4}E&uGx#m>$} zV-ZSx<|h%;%f-D7wm~9%@c?Za)%+x8l())h(z%bxH)eT{uuD7cCCXtP_AzP<-wtC> zv>Mc?)*3gmL`MgSpsfFR6yT&PLhmI7^VKMSIr>OdHtjOL;E>Z>SnWT{^;Xe0ydk59 zF0=XbQ|Blew_g4v(i$wZVQUOnKMLj03vS1-HuRt~`_U=cZ2f#&K0;{(MP;P)2UFk%ol7 zI2G$Jr~gwg2$5mOux^g+nmT0-`j8n2y{P-_huO9G)Wkqn$H|P;cS(v;MRAa<74$-T z)=r&b`X1JY9D-B;GR{?@R#9w7UzVy8@lX^glKYZEUU2yb^?(9BMfkK z2qdo(#*FtvIi?pV?)qO<|E$S5j9llUzHQR&;2q|<$8)YCm1L|-)4o9<)ZsE#O-MkVPOn}a`d}MQV<#1jQ8N2zmT=sjYJ5BAFPoVpZ5Mms+!DCRTQPWNY+`Q7eX2t-Yp5@q@dc-X^iX0bQkHiP?4RP2^5sd1XK zkMoxe{2Y!kU0N(W8GdA9E2QhnSO77uDEGo2ze{4Qk(m`Jof%_n&8qXC8zW+mS)V`( zAq$|^{Y~NM>J^{9iR*zo(LIv$j;7rp#|6g{z{>jG&cow~K)eT`KsUz2({R3tVSRrM zL(M*_Go>`!d2<$asb6+*x~?wIn8)-c1GHO-&*bQ$(XE1ZLSdX#(>~F^EVZCWo-jVD zTA%Q?7|DKp-JQZJ(CX`GBeI+J5O4S3O#?`QBwEKSK9!fwpQJbz`?zC``qGyLVYcPX zD3|Qt#IEIT5!Pj%kBi^K3nf|W-mDG(2%2Ns(huUyk$n&o_a2xdY+%25)Qevbiq;zW z!cf6Bhnzc2y4an8F(OAb9b7-?zt3Nudf=;H-%=`(gZhA?k?r`cC_ZTr=u5PC@M|m-nc&hl%J^FpktkretYBh z?PD@@tx}}4S65HxJ};j(NuB=FuS|UQ#on@O0UfO~l6~3GlEY5GE_m5v$r%4t@K;BB zw9=wPTXmxmk5c#1KA+g?RC#e%iQ7_Xzwcdw9ytyQnYsUck&@D@YQT&EcV}|y(v*0b zyK}!iq&X~|!6%U9l|YXyYP3Yj;`V)GTgGo#9W?G;=AO$k8NLXlfXTOq4kPjoJ26rj zB3ECynedtmFDbs#M3`l+bc4Gtzu}9-7W8S7O4IiK%>B@_A1tr|=!T4ZsaDB}$D9z? z5UaUmg(BOuJX-)^eXM+b+eH7T@N`PjBwgJ$BQ+lva>i=UTghCEcWsW!_57#6F_F+Y6Hqm*EdCrJ*o@gzUfeJaFO#lrZMEY)QNBiOk_rn&afOh^&v!?(R78?An3wc9n;#sA8-q!Ljwm{Az@$c1p;TwGG z3irSea7wQayp22?0EA<|Si#o68ZE&Ja(mE1JxfCtm>Y;r844oX<%xy=T7Gntd%#Xxtjp3DQ>87 z*&$j_qCCnKE&#`R>B^AUQutMGNgeuDNm4=qNcuC05@GD$>2O95neDQ{k+gtz!|?;I z;CSUEc7Wm_j(L{^2f&~;d{%#O;tP{@!%Dm$`O1q!_%>W9a{N`XJ0|!2RcuxOx%15L zwRbxanBB#F@Wf_>Y$fO!%ql~pqpprVpOI>;O~`dy%2^7U@<3+GX$Uetb7El8h^))= zc{M27SG7FGIbgZxU^aE!X{$qGd+uVdK>9+-E%FdK%c1cfuuxYAKy^l9#qBzEd zarD(UW#nv-!#T|#$j}4i>l?WN)t}FA5-3A%I)S4eP!s7*14#v4A>d??JP7U-5ztd3 zQ%K2AlcF%e0}{vNvY}wLMr6#~Q*8|4p_}gPmm z0X1XOE+-ALU+Cy&~JojJ(Gx%U>&H*iU!bZ@Kj(3ucSt|?F>nWFlm*bq>k>w(?F!$0l!UEFKm4&xk#zd9+~5yA!WV_CvN`~?W&cTNbYaWowxrKx`7g@p6b!#I^! zq8LlIFK5F?7lncI5grrqcfxMcW*&&prLy-L*2zplH5_UvJNjYT*|}w#4&&hiP6p&_ zY*2C@s}^#O(W!N{QLCb zzKy4CkHV<&7e7F^PoU>aNx>6l&+Cl|m&VLVH@5;|`sej-u8$Ddbj7|3n2d|!+u6(# z-;TLU7)*@16XetJy4*`|ly_a0VuAfcv+?I!Gzcy6l} zs!C&ihvQV@bfa4Y)&=~<{0ExmyNmdftz99zQ8bZ()yIelMTTVKY{W6YbIkc8->|<; zX79Gzj#fkPNWGlQyA H*x3IA=zj~i literal 0 HcmV?d00001 diff --git a/openpype/tools/publisher/widgets/images/options.png b/openpype/tools/publisher/widgets/images/options.png new file mode 100644 index 0000000000000000000000000000000000000000..b394dbd4ce517004dfeb6126e99ba2fee2b0a34a GIT binary patch literal 3216 zcmeHIeKgbiAOBKPR_;Ywp%n967I}!&LS*jsR3s1A9M=*`nLMm4$(7`JVq}bpB~O*m zGUG>h}Z&1=u0svqG+~v>_0FakF$^&cHNN$3JAu#~RC5JmYdd3_%?64i~*w96C+xx=8fVXi@ z>6-1PpFxj&>I-y^pq^Pqrv%X>Yv4kB89%XKglEs-*_SNw$IQ={Dw>ayH?991$j?*2 zEv-|G8=HBe>T@nu-$JFTs%;{~Y$~?huSQ5ps*AR%Hwlhjn`3mh5L~vgEU2$W|IE4; zqiW8B59wzK`rwTev-#u21UL7>1xDyDEkR0?)60S{PwyjYZ$HeuN7jnh|Hac-WQLf` z3}ZW}wm#iAxK6brEyJk$;}kO2tGG?Dr|VMfoxJx8RDzxX=^D-0$>Nv`R>y;IN1=Kf zuIR`8rn+u}+^-Xq+4MIRs953;v)UV`niRPuAAHO+b5?bJwp~5G3LjRpK*DTBtF?I# zzq&9$Rwjadb~SGZ(;SFyaBkw{gsQdZtA0SOcT+jpcKOOv6y11Hi)o+WMwwNOi>vVM zw}xU?nYxA10@`|ROZH^lSr@+;05H{E8FC4gus8rvYJwlK_fEjSW8Tlc{$k6Tf8i{v zZ*F}3MAh)>E^fbap#8yQTQ~^gqRv*u2CSvX$0)$ncj8^(r!y;WU)HIRquP7(HZ>gO z9{1mo%wA*@Y}f?8-E#x`#tZ#5BA3H^_I-s&nE$!{CLQRgFO6&iT$S?^ud^nLNgu|( z=CekN(aU3BzZS6Gy8F7Bh#gWX2g9gfw{6so0)V4v0GgV9vlWgiyJZoXB|PfkH;M5?y7{N{9%HzrD2M zONEVJ-Td=o*x;?wq&V-}z!nD(~S`EUEC=N(nEWg#3 zz{-CJ3y$ey4wt=DmL_&I4Sv95(a}<5=kke{X%j6Y8d3~jB#ob@=R@Ap3pdFqlp80~ z_}m@)@kWh#Gb?bVVlN>dNwV%sNuWomNf-49XMRA9l(sfPto3E^?QV;m&m$6lBf3i~ z7@FU?81ty4KiZCVYx2Cn2{97CUwjjKZsthg!s>bw+sSSuW~(Hiz7pgHjcI2GO*1w$ zY9&)RKc!MQpwh=Tkcl6=>!%@^ZU9>dhyw|An}H-wk8m$T8y1j z*m<=!)N_Z)7Vq8&9q^@@i^J#TKsqZ^*EQ-7ZuDL-U})(rc+5HNqXO|st&bE26b4xj=8J(AJGC{0-P(&kO3r6>j%1uXh^QI_8#}*y<{Svjnl=Y@ zT+S+DUcFHE?CNONBQ~kGzE`2#om!=7)~X`1!>KfCM$=>Re#rwJWTuXQj^C37wn*QC zN7YHd5DDpS9TKO@L!0WJu%0Yq>$FO0w$&?%>f$=T;s-j@qPq6eo#LEKfml~7ri>+1 zN}0jEW~W%rD}lhG+ODRG^{F2I(2yPh(iT$@(nUGdWdZ=Zw}US|mUQ`EungQ>#`M4h zC3olOgBck(QpObO%aZpbhBE%wdieieJ=<1$#}~2YKKJwP{+F&Ju}RHAUGhu1zixBK z6gwmCY;J;TM#~jV(jgNx?HppJ7%;gZvmSFqkVR4+xz&y6L&yHtc9 zG5B`kLfHL!Xe?_Ve*~?S6p58cU}!}Kr19`1;5;E9pTUHEm)dq}=R?@)wtnzOD~8{< zDDT9W_HRM#?lmNyiG)@OGYGa7`Hg8`c|E=CoX)fU7K^i_!pF3)1x-V!1zD=hGvB|L z2srcK1-!NOI?G8S>k+;V)x(;GTK#>kKX&RN3{p3pP#`xq_?L=f;-4>b@=YPi`WX&R8DcMr^AU(k!Gd#8%$Z-AL9pK*v|noU5d(cuQQ- z4h(rrmp=eD7AV-p7cp@6{I#n{Z2MQV7JI2Ty;{I8tNb+QX!+uc(d+ziseuK{@j^!l zQ~R@XCb?LOX9aYC6zMmu*0zjeu>*QjS{O1kBO<*q=m|IxUpq4UvE-f9Z9!-Ch=|Bh z@sE`7WsQl)!)hyr7%UeBK^F-XKP@ikP@&C_f3>QR<+q($hrcLn8vZ?z?BDr&q3Dlk zX}t=2aSYtka=<{5#6hy&6HNTYy) zL>mQ37$OixfzTkdB16DHlp!cnI5GtYVM=(%`)=KOe_!2O_s@H&N@dqR>#Vi*+O@v# zTS>WSW4TXCSqcEK&+6QnO8_9@B@*o31^=wZzS{%&Vc3Z-?9%gA~c3~qnz|g!|0FaE@rWpV&mwK zAHF<1Qu4Gg?4yoXhK8bivku?sfv2_2{f*(v7*$n?fIOOx!>bqHIrh6aI(eiY%X9vj zxai&7Uhr4<&2QFjM-|FlP69SXO5XCOVtZ2s6R#}ErOz`to^QzCYS;f3d;ioA*;biF zB+GXSB?WhL<&6*Nv^{*k(?VcnU2hJLiU-OC`zA*Br43&kIeDkzaN7|z^HDjs>F+i+ z%#}7}3DiI8QH&Re8Jq&9Mp`kikN@0){;L&5u1X_oDO#%RD{kGj=~v;sM1jFo(o#bL z>(t!5c}0HA_d;;a&yQVBb}5JNWKZZh=Li6_4uAO~VxO5t1E82~b;is-Hh+FJzBJh1 zjKf>>(z^5Z=EviYbxw*Ukup(36#lMrO?UA4r;Y)x@#mj^NV2!1(`!=bQfJcsMh{YU zm7x3Q$@s&5o?WPesk|kmeB*b z{cBspiyi^SEduAy?o>Fl|MX{Y;V&+r-CXcA{$uk`Z_ySF`W^bCSIME4&{0Qkco7%R zukr3EOTMQL*vY*mrK5ArW9w?X zGa&7&^W?8Wey-UQGVy=kll z#Ifml+!CV6576jZCB(|jUnexVZOZCYBBfy+jF|#9)|MhF<;g8S=M6%8{DfLaMz;PX zV~uZD^s%ea+o}+JsAyPBnOTVXm}SWc}O2)OBp*3n@pDi+OA#|G-<$B z0o3Fx0E_t0uA4qH8K^(~>zf)Vo2g7Ql8(0oi7E2Z*a?ax^4$x~fnSdlIQ$6LRMn6W zc~>mjm0!@rYlX}(oY1gxCnxRH&gbcT7Js*;!%9M@A7>IlRwl zh7QpjNFc}z+c!bgTU7%ppOk?`>>L+tt@c16j%4N4O>${-eS=TmXL5SbMjkR=9$2um zUw=xbX6)@*YKA1P>&$gdSV#iAu;wE*IPqOv@O^?Azr3SGQ&XklJv&4uu-%|g?D+jH zT;j9M=Ij|z`kq4_)5W$I7`g`UkDTWo)C_a(;;jCjtzwksVSupl!jMDR#xj(Gi^gFg zMo%{Xe3G)ZILf+TXr6FOnj*j{rlTrtcWUF>#$ttIiJbS9E})czWW2}rFF$RJ*Vu^H z*rr=QE}od7&ZMKlv^^Y+<0EfZEw!d=i&s>PWnKd2M?il1VOHDb>NknKu}h2#c}r2O zkT13(o318A^lKQiYV~X6x1A@NuObF60{zSgie>cnnVlO@f94ZZYo`mb)6hb)UNOf( zF9Q{}J2>E|L}&TmUOsCGxO{|tN(#{3Uh-d|Tkh*`5;ZIx(zE+}u01*tW`1-cbu0uB)0#xygy$wWv4u{V~uu3nlzyWEYS~8U3(Q z#x%gxl8NoygEWl-3DD`fg;O3XkYVqa0;(bNa&sS65U!^YqzU5&et|L!E+$xUnu_|BssG~AN1V^8>esAVC7 zs)$|=K+3`1-}RIbpKY$0$2HC1v`wB4AvK->hA1B=%z;ep6$81av`R$rd>za&8K44( zp55qyWGEHbaq?CgONG)xWSAD&a~N`Hacttr?~o6(avV4#%Wtn)2Aw>}NYU@DyQ*k!OEGC__$9=R__n3uR|A1` zAla2FP}LcWf7`P2GLFDc@tPg~MxN9i`y>BOwr%W?y<&Qr*xqF(bf3j%9izykrHPS? zfNDOK;zz%*Y3siMDKU%}C?2EX4ctx0-FxD=p~U!{am~~%3|w+#;C-BfDLZ`M9#t#{ zsUIOz=FuO{%z3S@?>ZH+ z+9<|0F+xrqe{ahnv9Y0kv#r&ceL{-Sw3PyNR-Q)Qi6Liy&d@3q&5VCPCo3^tMZobV zV5m>HRZ%)Nw48v@570syjoG;>*Ho+{0wVqRaG%Tcw4yEollhMr#cec9mgC=;TqO86 zqZ!WO>G5G!T{z(d5#xg~Z7O!^#HV}(34A^36K=)96d!L?w0YmH^OZA=_kFE^u)viC z#j{LS8EX+Hp6G`{Wl|w)n0nq)5}_|vYoGC#*bRNLstU0EYu9xGs9{QAI>Q8#o1Z>$( zJ@_dybe;=fWMA)S?v9okN3goPh4Yg5m{Diry3IcB!BS4(G0^+hq|97-1hqH=)f$~! z2AtHyq90ADZk-%BO8N)tB57u|4l)#z6WHCKbqAyvStsEX7XKKTx<|+@0SO{FNOC(b z-R@xta65qoj6-2BC+^7u(&$8N=h1F^prwum<9GGP!hX_G0BK6OHz&}=PXU-y>ol~n zLIAdLdQ5)*1&Q(BVNCn<7b1gfIUy8@C%6EVIrJb9>6mg%@P4fdyWEd{E!3y z8n6S#4S4`^@4`cw96X!hPs2tR&i(I=A}>dh%p(O{pc&Sen5<1zBQQ+mYDNR7wRcd6 zfG41|?Lwj?aAd%t|Mg)1OuMhXPz zi42h&j$j!z%sm*OH_%=PXx3_uO-72?)(5c>{I^g?tAS(pBj80SaBI@aGR-fcx~@IZ z%s^H8N`ca@2nwfEO9d@nGvb5U>W{FHbUr2l1}7v~iEq0T#vkVoRoXV;Bk!JiI6G1m z0Gz63s#TxE2x`NZE;KKFeL*e*$#5~Q_bKVuOl0R9QYy-bevxXFG#oNzqU-CY>&=%E0V zCdrei#N%)1tQDB)gNk-dD#LQNADU+|nP&K3)JxTv?J~0Pl$5+k_FGP7raSHgM&8c! z-JJB7Af2kKj0;ekl@3@+fihW*mXZfrVa_d2eDqp<@KX|G348P@vMq1~O7jquc65xU zL8hp2mc?63^krK=UiDftaLU`Np356}Fq>5jtKtSq+zeNivB%wE^apIa*RsDjs`Cph z*j#BK*ds`7VK6K(tgLG@da76FHMXw1`i$2GOCSEuCFo8P}dazyHwL>IXNgS zjNi^Ygi3|o@b5Aaq=A#&CRD)ib%*gG)TQXX-xo`Nlm_kaKh|Ae@`KG)Z~xET|6V)O z?Et|!P}tU#L(12r3#YWzaJg`*|7gI&iucp zeVg^67#_I39YzP{ z_^&xj0Nt)=$zh*|1V;p^yK=NO`hhR`Q3-endn>x!czqsjNkcnCZV~IncQuB8?=2}1 zm_3iG>H1-74tCaSDT)7SQ4J0BF@Nv7rO6~n$hrv&kvPeE&wNhhsCxp*RE~Yel`82_ zn}G^f@^wM1pW#!OhjM%7-!I@@Us9|zb{TqCL7n-XU9l(LH2h>V~ghfiQ5*k^D)BEB5 zq-R7qus5!W^Ij2&@<)nlQmL*rjR#WoN?FB}PF#o@dXm;1cQDS5-&JUS{E;3`8;wTl z_nglQU9PA%MR+DXqJ%%n&8KjI);;6$zYR$@VVXW^KkI8IRR- zfRPdTJev^q=5@=o2jV%_rKxGk0u!2Hy{MQ|6BuKuw~b|_5NL5{x(L{!A6BG(#HcV@ z4xp1}M}D72S!^*i3bwo8&02}(^35WqnNwODTRYvp#nJe)tcSGzSQM*@`h_U05KeBF zgcZ;U_*SHn>7H)y;9DV_tC0L2x1D(Q3&$271?06Qh|UtTl=P}=8_|Zzw!C3Z)>FJWU& z@ATjb56Sc(hNl~{1L}fR^S$}E1u&<>D#&Cwihp&vco3JY@j12WQ zyv);U+^jcKvWL$pH|h;?`=PU#3~jVfq}1`Sqtmr~S#VJg)$PF2b|7EbM&VWGoT`pd zJxdp^Y;RpwI^5~bN-S5~zjv7p-RfA}+tHd96dJ|%=eH8IblXXXiFnPva*F z^ngXjc&bNcJ)5UnZ@s!th--}hX+fP4+)y}WQ#MqaU^rd$#6P-S7tO7CEk-G>RlQf? ze-%<0_I4GdZV6`R7gVUJogLSz!UOnkvaNP*L2cd>a|J=heL-2$6ir96r~3;%Ez@ZZ zu3ELuIm4Z;-4pVDL9IWf)=<&yyth$`~C^Pb9Cr?Z`Xb8*Y&wmPFR_8unMw55X31RW4r^;@C>>Eg3_-G4;aTq$j87iy)O(%r%hR~N zt}e>g@%)>N{9(tN+wzLu#|8Yyb(V~JZVts*ldBu-bB5O+(N#Put8&F{8oVt1KZM@Z zR3~dUhrXYRU(|gV^B!fbR^rvup#0{CqJ+)?n+KjpZ>*)5$(M-}+A99pG(M+gb~2`; z^kvEr)~fpJ)Xt;)jVt8_#8L+7!J%7>GRAsCDgLR{r?S$4OJBIZki_^7J_}iHdHWvo z&~+zlIQbg-xvfeSB|c=E?}D#{f}%=iQIDeU#+H`KFR6>R`%{#@_T*g=5WDg(zY)8c z^^=*~+61(2SoaO_4L7}wY(=vg>Be4GW;P1Jb3a!&u|h^t+Qj%Zyq;TS~U19SaAAa)4E1Mwb94+ z8o}8NJNX5o^q24aex4tKQ3H*E?|*LzeKrWQe>Qs{BI3Sf?@&)1@=l-6j=pe-IgrY} zV1iR91Sw1G{ez=Q^&%ih6e1cM*hLl0f4W*@ivEApZkmxVZd;K1eMiu`4WGuK#; zuoUmekD5Kg^j}&Xa(B6YV^rQsYh~?%2&#W||CPb}ekRpOdja{g`dQCYAo;RPeNp`b z2Arwbm$gfhHS_D}m(_!B04^CdOr@o>NC91${*B)d^4tO&~ z$uoi|DqBhzQy+iPLO$ykE;w>Oe;ZOn{4-zr-Sa{l83U9huYF{S`24+>cQT7Wu`USkmq(!H}0E8vP>B z*93~YLJrzQ0Zfh@l!M34X4F^x^{di{smML-3=HP8LJ zo2jufB8^K{yBa`1Q9o0^KX;uYcE`@Y~Qx7$LtA>30j;sHGf{nls^#;HCr3ab$NJB0F0jCh(4 zd{3x7G3>p&-;EMdOyb+(B!Z_Rlyk?Hpt2nDyeR*N991RpLP-*Ve!J>-LqSvR?rNU_ zCI6G@sNx%O{V!l@>LqF}_3qxs&-%`Mb;`Kp(VkA*oYcmU->2|%r~+=R;YAM8MaEFe zSNHh_&bqr{Z_Qc1C_yG;hgz;J=jHTEcOX}hH5r)Vh~K=V;y8AhghwAEC`;~tCq*Bg zkg=RED2J8 zidPttxDIArfcG9P?1?UROAVZ1_b0Z z@zSqNi~6DWJ~n^(regV0nRKliL%Qb6NgGH*k<{u>(ryiFqNYx;4Z|N)+l6EjeB~vm z=EX*3*g5FDqc>SYXG9m5oYOi1WxyKfsS__o;16{=C@>Evj4I&58ukE=pQ*2wvz&Na zf-Kv3in@0ukF!d=ecRY3@(#&_>ISak8Ud-XTfgXXpM9lBUk6lbut6M2Ye!4Rb=7gC z{2mub@Y)0doJzRbisI|7I35AnHFr*aQ(5uc$G8IKE3-iZJ*yr4dO^rC4PK~XEV~#< zK;ad^;@G(ss$xd~5yG%nFVOQvlJXH4~XHy+0(*lsfk6)=y;iHG%ANJ*vpo$#7 ze&XoH_bOcLWhcRcX4sAm>^$?U52i2=%Yfa0)rMc>=JpM4JB~ha)CnrHS}H*jIO;;f zAoXk%X|~BW3@V2ep}*_8FeEiCu>N1Lr~%4kZZ-$AK5F#AYWMeh2&?i9CpMk=IQ+np z`;YT?y~^LaUa%-fpbWjZ*L~=ZPeet+;LnbZeJ!_}1co8tN>MeS!GJcz-(M8AfUbQkI+>IU`ou-5kmXnI1KxVoS)MkCbZ6Sx z-y*GL9-;_EN2)S->5??vc%seIU8Zris$`4}D;sGhMw}5zP3bd`BxXNJwquEv!4Vb| zp(l9ZD12~O7#ES&R1NJ2;t|;mP~N9!Q~^ronk4c22qKmoHny5hR~!jZpigL`*U@xS ztqpUVE^kOhB0zQ7tYAs_HHP}4(8Aerwj`F^uX75jWv;n_AtNjAf9s=alHLQYAzFsZ za#nMk*Tq8&cDn8QS3#m*b}gjx;FSw|b1Sk=Y3B6qso>wYDUY@HwFqj7;|Muf(~yBM zM=W-|aaxp2QO%H?{}HVh>=b~iUAjHE?AK@$Gd+lq`sZ;9Mx5A9D?;p=JW>A;6>y1_j-=iJCQZH( zti-VMun)_`skcM?@GhegvI^dg$-2k!lP9!S#OE$zxm$L%L#qT#OM}O=k;g zgWc0GHl+`EAQ|%ArJ&I%vg9iqSIhO~BlnM=r`iAZOcd!OPiaaKjn<%G$(cw0zPexr ztFo;N`^v}=$+H)^%MYE{c44~Hz`{W8oj$w9PoJCSvOoWC`|oU0!VQcV(J!r?7=t|6 zE_`p<1uoU;w34|6}CV@y}1ekxxD7ftk>4<1);I2rPKf25E zR1#<0VOH(>16D{h*;AV82y`90;~^~NRA~e>el@#8Qf^$Cx>aJdT=Eh_ji+!Vj(YB*612lIto-+bv(+fSEWVW&8gZ}yBzQ;z zyG%~`<_rmOWA(|D8;Thi)ht&w(jVYy$#z+a6W(_`;X3}jLs-MT(Nr)h#762gL1#*q z8)itnD2!}Q!KlJ)5K-{%NqU3_Pw)kwPw<>!^glg;l-@Z*8t?qs|HG zn+AJvB1b){MMz6zoi$juoMqbkN?79RyI!rE?6rPJAG5QOYUUt<&B`;5*t=>owF{FS zBB=Orfkl~cloi5?IlWfaGkYH8%7s1qRA>mA698lje%qq_nYMF6rI*~zVqkpoFo;w{zs{ZZ(y%U{`lrgvYv4FVCyp1g9N@j>M&d|#X zb<7%mLf;;3g2mKb$cC1$$j^ha3S2A2%RpxH;6n5Os zcA~uU=p>KNUgD2nosH0*2?*^-#T>ahpJ+}pDoJA6+#h@*Ecw?(1!xsrla3MdI(sx& ziqmraHsX*G_evm|POv!~EHuNW+Wg2=stu);H!D0}`SGkxrynsaQ*HeS!wy*vfs58L zbJm;hDwFkgq|&EWI~ykKZ3tGDygOOY36GA<}+U*zzH>E~axptZ1$q{ojl!~6&L?}m1Vf3l8lbnUuD zK4!XHFZAP04fH19aPKXU(Wea}7DeMW3R$aC(y~j(h@IYYGme&P^)EFxWP?M)lHt$n z1;EDMTyzO5D*TgDOA|`3=#-}oRt1KBZqud&c%9Cz1 zP%UO`2}NT*z040!cJ3^Bxm$2uSNNXxsa?+q>2|#6<}k){n^NrQAb^K#9@U7>N32HE z7wC$XVVVpa?eLC4@U1g#IX_(gWVB{)y4x3|&fy?l$VPn-Q7avg6yp_qOQYq0RzcXi zvEQ#-K~~dGDKENk0nB*e5BEy9vrkP@=3~@_?hrd;1%fd>9i;QK#v}djbK5_$k{;~B z4vke}0Uk>>bP$~b9y3SnoAK%EH&v*;ziqlJ9m*|mOu`)r2s3gu9BrAsJT1B@!j~B@ zy^FRU2!w6nLLhLKb;?r|IyqB`$d?Ck;Lkd zAFiTE7bEb#6FVIey4N1)q#=T2{#4~R1W_xdA@vKJQ%E+b+>tMP^z8&|@BNuA-Wi{W zkl=KAwf3?8<@dKsKe0l1sJpL|H0j%EZ}zcD{LkDmVeC15%OcS`I~#xmyMjnW?ekj+ zUB`)Ar~+Q>Iq1Kkh8!2z1FhdWY>~fiephWdc#-L}c_S~nLMQ^FAht14nDUicZ*8_H zkoV62Bpm9FJ&a5CTSC#^N4bS%3aiCk_TQX(OGc5FGshB3 ze0LrwnCVS=%1#}A{=hoZayJ&7)jf!#8`Ec}MFsOK2{EXg5BnU2u=l*dpi_4>RENCB z(t*5tH9O&uo5|+L)H7+1HNXOw16U6h;2i%MVSfsZ?F2;ZXBhsD$I*wu7#?_fE=MTr z3|jxiD$bShut}A%0%BR69wTp>OZ^gioqJfaMgw%>PT{qT%^5Ktc<~12@KvAV=(4WH zVkCPC7Lwx@kqBUyNL$1F{;bhb&h2HZcWU)wC1Dkc7r+{(MeYwq1TI#lq(KwXy3%>s zCq2aKG;oBg5zK)S*rf+s>UCwEjjHZQr?aHKK%q=X{mfLJO?x*5tA%;Kdp(!jqi zHXpUJXzY+R;`4rtL18eRvLxo;RaR~`%&R?Fg z%pOi#m+v{bqm>ll0zEwl!nDW$cF2Vo^e#qzKIY1W3qA)D?pmu2B07iGTVp~Ha@Na- zyS%D|=YlngP$bU}_4yI=${OC*AQ(gv-`5Pq6%4^eQo6L<3qM<}@Z{5Hx#ON$s|iEn zEdI7*4~s@M3MY1-%N=3K|s>0m%EulUKvAu!fnt)SWj#FaF0T`%Bh5Y4kFa^%mLhD2e)Zhvn=hj!9?8`_(hxC8p}0Z-gQ zN-3L57`j`Zm>;my{WhtO>^Ko3G6BVn%39{waro&`+)GF!yT+2bcy ziINPd*6c8g#w-`Oj*HK%Eb=FzmaiR;BfPq6KAI385teyl`{r`Nrb=;=o&Pc4j4Q2u z204%BSfNU7_2>VnUHDzJ@9CZ8JU3hE1j78J0%;P=n^UG~%UVh`vn|Xal&Dt^%=W}l zDtG4=YFB(Q!O9GoJ_A2B)@`Ytd^J9VW?$81%K>M$IU$gQeZoO5#mF+PsgDI+333eG zeBBKrXNw5ta%fmX%iKN?{Y(gJ==65}GQJI=KReLc2g_fSWFS2{rbUgG1?h+*ZSsQp zw!J0;5^V_4W_|qRPZ#yLN|I@obGH;eefe?I16Pio{IP&`xr8?a_f+IK}jKvF-`gU1M zz&lQd(P#R^j)d{9bhJDOjDXd$=2%Ie5c7mlJEQz=Z;yT70}uTXrx#3f3AiAI3`gB+ zjZ=xKa66S2^)vBOpV;dqCi|2c%BQZ~U^+M0sF~EH@pXTn!DbWd`blt*%pTuap&{rz z4iqOJ9`2_XB|7yPu-1vp*teC+uL#+vesh2bA5YHS{^<@`Cu2@#YT7IuT`9d=Qeho| z1*G&K7g8|ys{+JzHJT6O2pQhUA!M5=TJD__^#UlM&Y)?pRRYE*3^*fIX52(Z-c%#$ zN>F&H_IJx2X!nrJZv4{&QLbUh$NVJ=D`|P3lv^4VAWEQ_fAs=WfgrZ{gamb~*l4-@ zWvKm1Jv99mU0KVNsS0Pw<{RfQGfqTb@wF^znq{9sbX`u^ z;zaIl(uQ)dnwOj8JMn5{9_WBMwG_SUMd2a&E>77CztK^Ltaj|RShd7(#u*y09;nYO zUnBITT9tA78IQ#t)!L^`8Kb9-e5a~rvt2rMfhOg5cTYpQIC2woVjzmy_oN5W4DT}_ z4!k(%I_1)ySrqYL@15bb89z&<^*Rl7f~F0L{LI3~13qz`mhMXgJLS5;nJ)L{i3;td z)dAx3H5!80bNPR>;68m#vIet!sf(83=)pHv??sc|^@21<{#9De(G8}iLU_hvdmWe26=Qk2s%9}xdDlhprk zZ`u=_l0C7r1stbD{+svgZP`TWD!2P@<|9HNXc7Kn^UT->W-B&oBB4lTIRgQtzv)%} zyKB?X$0k{vzPTerK!RKa6oTb*ki~NhN&2V^j;dA-&O9JBU!w`Sb81;&k?)bx*pxuT zAhoE}1)s+sjd>(3ey-HW9xt)!x%mZLyiX1K@!ac$l`70Dt; z#J9=g^k2A{I8AWx3FS5r3g_Emp8&;N>21vgHj!jGR@e&F?^54x{x- z^PlX&{mTxk z4Zv^YEjo{TL;wdfbIilk!xki`il~~-s~$<4d*&F<;sZxcuKgo?gl`zs(-!GiKo{vk zi#PJR>GkQp9;#`vks9@m++y1omV)h-U?;b*DIj%QZD5unx<4=7Y(^c~LNG@y)R%zgg=#E1=c`p*Y)N1b)~)`?-smt*-`lyJsHX1b>KwN+0RfQW50OXFM95w-WKI{8ank+HHM zl7iv?EB^i-1L{|Bq_crMLakMudX(4szes`qRYYfY}d`B0g-XmXwZ#LUzKXYoCGr2irPDKTRA z$mxHzekm;T=C&R^8|>C3G^`2s_d@^Op@vr5GziOlIfR6jMFZAcp3vSC9}ye|2%XNf z<0>|qmBd3ob5?H*S7Xidp|Q*18RCG!d8OH3`OVRgWzM^K9d?q!}39-@UQA*?`5 zNK7SUq*bS24$#GEudPb0cOdKMUG6B*c46?6qWf}g{Vb)5QKlxwzFL6dqjy@A$G5w+ zZ1dbe`1^V&Gy0dJjZ5{uKxsFQ1UVd`2NX$L9qG%%?%ax!g{d?=MU_&9G4V>DPr+p?qIv(CO3McU)xZbmtUFw^C1XB+m%9R-DmTaKTdfJ?Gi^f7+w^gp3um{!Lkfj`s-oYN6{QJpR&Q0$)K|LO1E$F6dS})T}Uy> zdOut;M3B}p?^jIh$L=->*e@TB%sJW+om!5C)VsuKJQqI(FpYES@J8{?4C+(Rq+M}c zbcfJ7Zsu*x%<1$)yJebAJg=Ru&N!wel=XLvk=lL7x#7>-FvEh@IBiYY`5Le5TdXIt z&Rl+|Pa2O8p>=lE1EBsA5G%y9f!@3Q+xop~7^kyOO9^Pyk}|yv07L9}$PKt(<%b9v z;A{`yHN^Q)Du8D)@J8(3IAmGg_9|+^o#VFWUR59N%n~a&ggk#yf)e_Y?@yVZ6Hm?_ zVV~2V^D+Bg^vUeqU*3075dy#$|7jG+xUV#X3eJ4WoZYL^U*btad+z)++jd+Si+Vol zZ?v@#L0P3^?gu9@$4cU^s96ADx2;|V30knls-0QAi9#Km1B#$^@XjAyzzI64#rN@~ zTb zKi~=K)kMoxAL`JrZub{Hz#;;YIoXkr$cspHj{lYAqhZ-1CXGl@o*(wq=3h#erJUJg z(ameQ@z&`m5*GSC%}@8EU!e=-KYN%N?`>*|&ccD1cGZ~>9k{IG|!<$w=DyNp#@A3Cm{b8Yr9a4`T=^=H}B7&535ITinihc`)hcu{+IWbMHdp5-9=c zfbuB^wJC}!M~w&Q`WZY6s3?*TgY!;h)v!|@k6COswA3Aa#^%p%+24jGm5?TJlls4( zuE=t{cuvg)wFwZ!_hIAFbLXo>*W;%azn{a?S?G?mD+&B{0q4c#M9}0PEsEb9p>3{a zo7@MJ$7n^jLDTEo%`|sJiTdui1l2RqYx4CtMWJ8>{MV-Z7CdBnoj-e0oXPODF zkA^ZvXBUGI@z0G=&#S4M%1B!GR{85d9kr2NShezQhLpO4!;(uniuwdJXTNyETVdBI zu~myZwd~gEsUZzGeX^vz2U~E^UZZ-?QVHaIZ;6?%yZ(SG5IiK+EgjZ&#oq6=870eo z7j9tc9)TR|(H^E!F`U*1KfVRvJR{KHC(GaJS=#jkn>ZZbgD}pH^aKD-b%-^ZN=Xt0 zGqEJ!VZ&=^GcpAphVvf${0{|LIk)b&2tbiJc_|`!b9r(a;_d>`7l$KB5_wu>aa3af zu_%i*Dsf8@XR}fIoD0nH2loIIH~^uXia87b6^O(J5M{F3JsxUYpP2MnSd?fJ*WkZv zG>!sIl4@=M^mxG^uBTB3QzVH}&K$0S|3N%ZQ3k+uY>JK{LCTl+4m?M3o3j|z^&JC% zB|wn$_n@Tgu6AUw3}fX#Q0d`0oeiTM6hwWvM|^(?C||>}x0E1~6SbN$1_P8U@Mzt^ z^?$ARn*l&G z8@`GAq%M)}C&kCOT8`{3w2eA))!)nA3=Pr7;Ew%!wmjX!>&L!_t4I=i)L4a>T)D9nJ}Il>Q_${#y{*zS zY=*yadVx1iKadKEsJ|fLV?jses#FZE@&F!!BHxQ!@ay`)`3#>Hl;Z{k1k|c<=6)MiQtPshFWFDdmlumdJ zLPH%lFg91EKOy)-73zeC0QW1_AjU%~XGxM>DG>!xB2^cUkCIQ4Df0gCY4%t-Ez{C` z2MCJA4)cL(5|FWfoGDfZXFQPK#O(2Oo)m=WWSYv9%$*k@wEa=HvISaniQ<-zVd(|W zeLh~IrV^SV!H4~kutg`1b{W9INdROH)nFVD#BCTg?%f4-ECu2IQI^QMWI!*ap8pSe zh8$5smeC=Z0Hq&6JcZYRF1`5hO&xLpK}C~3vd83+OPOH<38%v{&-i}zBxMVroH`s zkAhE>sJkdDseq+1T!8YPB#W5tf>=maQ;W>HySVEYmPrT*N}S+~O%FSwBam@jQvI`L zq7QV+7@ab>ilIimP0WC=n*i3^FYwZ938~Lb@T+iMO=Y44=@b;n>;ncS4QTB{S5s>s_-YA|KV&dD&jRbOZ=;4{ZXEI;D>B-dw zKAhsG2T-`dbE$_xh(-7rAC|(z3$<(K*s~q}bIs{&zCAb@%g}Ho~hLKT1 z<)Xtpc^?Hrn>WKglGXdQc|6lur duVQc)eZ(5RB*ff-0?#KQqKTDpjiJZo{{v&Dg@^zE literal 0 HcmV?d00001 diff --git a/openpype/tools/publisher/widgets/screenshot_widget.py b/openpype/tools/publisher/widgets/screenshot_widget.py new file mode 100644 index 0000000000..4ccf920571 --- /dev/null +++ b/openpype/tools/publisher/widgets/screenshot_widget.py @@ -0,0 +1,314 @@ +import os +import tempfile + +from qtpy import QtCore, QtGui, QtWidgets + + +class ScreenMarquee(QtWidgets.QDialog): + """Dialog to interactively define screen area. + + This allows to select a screen area through a marquee selection. + + You can use any of its classmethods for easily saving an image, + capturing to QClipboard or returning a QPixmap, respectively + `capture_to_file`, `capture_to_clipboard` and `capture_to_pixmap`. + """ + + def __init__(self, parent=None): + super(ScreenMarquee, self).__init__(parent=parent) + + self.setWindowFlags( + QtCore.Qt.FramelessWindowHint + | QtCore.Qt.WindowStaysOnTopHint + | QtCore.Qt.CustomizeWindowHint + | QtCore.Qt.Tool) + self.setAttribute(QtCore.Qt.WA_TranslucentBackground) + self.setCursor(QtCore.Qt.CrossCursor) + self.setMouseTracking(True) + + fade_anim = QtCore.QVariantAnimation() + fade_anim.setStartValue(0) + fade_anim.setEndValue(50) + fade_anim.setDuration(200) + fade_anim.setEasingCurve(QtCore.QEasingCurve.OutCubic) + fade_anim.start(QtCore.QAbstractAnimation.DeleteWhenStopped) + + fade_anim.valueChanged.connect(self._on_fade_anim) + + app = QtWidgets.QApplication.instance() + if hasattr(app, "screenAdded"): + app.screenAdded.connect(self._on_screen_added) + app.screenRemoved.connect(self._fit_screen_geometry) + elif hasattr(app, "desktop"): + desktop = app.desktop() + desktop.screenCountChanged.connect(self._fit_screen_geometry) + + for screen in QtWidgets.QApplication.screens(): + screen.geometryChanged.connect(self._fit_screen_geometry) + + self._opacity = fade_anim.currentValue() + self._click_pos = None + self._capture_rect = None + + self._fade_anim = fade_anim + + def get_captured_pixmap(self): + if self._capture_rect is None: + return QtGui.QPixmap() + + return self.get_desktop_pixmap(self._capture_rect) + + def paintEvent(self, event): + """Paint event""" + + # Convert click and current mouse positions to local space. + mouse_pos = self.mapFromGlobal(QtGui.QCursor.pos()) + click_pos = None + if self._click_pos is not None: + click_pos = self.mapFromGlobal(self._click_pos) + + painter = QtGui.QPainter(self) + + # Draw background. Aside from aesthetics, this makes the full + # tool region accept mouse events. + painter.setBrush(QtGui.QColor(0, 0, 0, self._opacity)) + painter.setPen(QtCore.Qt.NoPen) + painter.drawRect(event.rect()) + + # Clear the capture area + if click_pos is not None: + capture_rect = QtCore.QRect(click_pos, mouse_pos) + painter.setCompositionMode( + QtGui.QPainter.CompositionMode_Clear) + painter.drawRect(capture_rect) + painter.setCompositionMode( + QtGui.QPainter.CompositionMode_SourceOver) + + pen_color = QtGui.QColor(255, 255, 255, 64) + pen = QtGui.QPen(pen_color, 1, QtCore.Qt.DotLine) + painter.setPen(pen) + + # Draw cropping markers at click position + rect = event.rect() + if click_pos is not None: + painter.drawLine( + rect.left(), click_pos.y(), + rect.right(), click_pos.y() + ) + painter.drawLine( + click_pos.x(), rect.top(), + click_pos.x(), rect.bottom() + ) + + # Draw cropping markers at current mouse position + painter.drawLine( + rect.left(), mouse_pos.y(), + rect.right(), mouse_pos.y() + ) + painter.drawLine( + mouse_pos.x(), rect.top(), + mouse_pos.x(), rect.bottom() + ) + + def mousePressEvent(self, event): + """Mouse click event""" + + if event.button() == QtCore.Qt.LeftButton: + # Begin click drag operation + self._click_pos = event.globalPos() + + def mouseReleaseEvent(self, event): + """Mouse release event""" + if ( + self._click_pos is not None + and event.button() == QtCore.Qt.LeftButton + ): + # End click drag operation and commit the current capture rect + self._capture_rect = QtCore.QRect( + self._click_pos, event.globalPos() + ).normalized() + self._click_pos = None + self.close() + + def mouseMoveEvent(self, event): + """Mouse move event""" + self.repaint() + + def keyPressEvent(self, event): + """Mouse press event""" + if event.key() == QtCore.Qt.Key_Escape: + self._click_pos = None + self._capture_rect = None + self.close() + return + return super(ScreenMarquee, self).mousePressEvent(event) + + def showEvent(self, event): + self._fit_screen_geometry() + self._fade_anim.start() + + def _fit_screen_geometry(self): + # Compute the union of all screen geometries, and resize to fit. + workspace_rect = QtCore.QRect() + for screen in QtWidgets.QApplication.screens(): + workspace_rect = workspace_rect.united(screen.geometry()) + self.setGeometry(workspace_rect) + + def _on_fade_anim(self): + """Animation callback for opacity.""" + + self._opacity = self._fade_anim.currentValue() + self.repaint() + + def _on_screen_added(self): + for screen in QtGui.QGuiApplication.screens(): + screen.geometryChanged.connect(self._fit_screen_geometry) + + @classmethod + def get_desktop_pixmap(cls, rect): + """Performs a screen capture on the specified rectangle. + + Args: + rect (QtCore.QRect): The rectangle to capture. + + Returns: + QtGui.QPixmap: Captured pixmap image + """ + + if rect.width() < 1 or rect.height() < 1: + return QtGui.QPixmap() + + screen_pixes = [] + for screen in QtWidgets.QApplication.screens(): + screen_geo = screen.geometry() + if not screen_geo.intersects(rect): + continue + + screen_pix_rect = screen_geo.intersected(rect) + screen_pix = screen.grabWindow( + 0, + screen_pix_rect.x() - screen_geo.x(), + screen_pix_rect.y() - screen_geo.y(), + screen_pix_rect.width(), screen_pix_rect.height() + ) + paste_point = QtCore.QPoint( + screen_pix_rect.x() - rect.x(), + screen_pix_rect.y() - rect.y() + ) + screen_pixes.append((screen_pix, paste_point)) + + output_pix = QtGui.QPixmap(rect.width(), rect.height()) + output_pix.fill(QtCore.Qt.transparent) + pix_painter = QtGui.QPainter() + pix_painter.begin(output_pix) + for item in screen_pixes: + (screen_pix, offset) = item + pix_painter.drawPixmap(offset, screen_pix) + + pix_painter.end() + + return output_pix + + @classmethod + def capture_to_pixmap(cls): + """Take screenshot with marquee into pixmap. + + Note: + The pixmap can be invalid (use 'isNull' to check). + + Returns: + QtGui.QPixmap: Captured pixmap image. + """ + + tool = cls() + tool.exec_() + return tool.get_captured_pixmap() + + @classmethod + def capture_to_file(cls, filepath=None): + """Take screenshot with marquee into file. + + Args: + filepath (Optional[str]): Path where screenshot will be saved. + + Returns: + Union[str, None]: Path to the saved screenshot, or None if user + cancelled the operation. + """ + + pixmap = cls.capture_to_pixmap() + if pixmap.isNull(): + return None + + if filepath is None: + with tempfile.NamedTemporaryFile( + prefix="screenshot_", suffix=".png", delete=False + ) as tmpfile: + filepath = tmpfile.name + + else: + output_dir = os.path.dirname(filepath) + if not os.path.exists(output_dir): + os.makedirs(output_dir) + + pixmap.save(filepath) + return filepath + + @classmethod + def capture_to_clipboard(cls): + """Take screenshot with marquee into clipboard. + + Notes: + Screenshot is not in clipboard if user cancelled the operation. + + Returns: + bool: Screenshot was added to clipboard. + """ + + clipboard = QtWidgets.QApplication.clipboard() + pixmap = cls.capture_to_pixmap() + if pixmap.isNull(): + return False + image = pixmap.toImage() + clipboard.setImage(image, QtGui.QClipboard.Clipboard) + return True + + +def capture_to_pixmap(): + """Take screenshot with marquee into pixmap. + + Note: + The pixmap can be invalid (use 'isNull' to check). + + Returns: + QtGui.QPixmap: Captured pixmap image. + """ + + return ScreenMarquee.capture_to_pixmap() + + +def capture_to_file(filepath=None): + """Take screenshot with marquee into file. + + Args: + filepath (Optional[str]): Path where screenshot will be saved. + + Returns: + Union[str, None]: Path to the saved screenshot, or None if user + cancelled the operation. + """ + + return ScreenMarquee.capture_to_file(filepath) + + +def capture_to_clipboard(): + """Take screenshot with marquee into clipboard. + + Notes: + Screenshot is not in clipboard if user cancelled the operation. + + Returns: + bool: Screenshot was added to clipboard. + """ + + return ScreenMarquee.capture_to_clipboard() diff --git a/openpype/tools/publisher/widgets/thumbnail_widget.py b/openpype/tools/publisher/widgets/thumbnail_widget.py index 80d156185b..60970710d8 100644 --- a/openpype/tools/publisher/widgets/thumbnail_widget.py +++ b/openpype/tools/publisher/widgets/thumbnail_widget.py @@ -22,6 +22,7 @@ from openpype.tools.utils import ( from openpype.tools.publisher.control import CardMessageTypes from .icons import get_image +from .screenshot_widget import capture_to_file class ThumbnailPainterWidget(QtWidgets.QWidget): @@ -306,20 +307,43 @@ class ThumbnailWidget(QtWidgets.QWidget): thumbnail_painter = ThumbnailPainterWidget(self) + icon_color = get_objected_colors("bg-view-selection").get_qcolor() + icon_color.setAlpha(255) + buttons_widget = QtWidgets.QWidget(self) buttons_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) - icon_color = get_objected_colors("bg-view-selection").get_qcolor() - icon_color.setAlpha(255) clear_image = get_image("clear_thumbnail") clear_pix = paint_image_with_color(clear_image, icon_color) - clear_button = PixmapButton(clear_pix, buttons_widget) clear_button.setObjectName("ThumbnailPixmapHoverButton") + clear_button.setToolTip("Clear thumbnail") + + take_screenshot_image = get_image("take_screenshot") + take_screenshot_pix = paint_image_with_color( + take_screenshot_image, icon_color) + take_screenshot_btn = PixmapButton( + take_screenshot_pix, buttons_widget) + take_screenshot_btn.setObjectName("ThumbnailPixmapHoverButton") + take_screenshot_btn.setToolTip("Take screenshot") + + paste_image = get_image("paste") + paste_pix = paint_image_with_color(paste_image, icon_color) + paste_btn = PixmapButton(paste_pix, buttons_widget) + paste_btn.setObjectName("ThumbnailPixmapHoverButton") + paste_btn.setToolTip("Paste from clipboard") + + browse_image = get_image("browse") + browse_pix = paint_image_with_color(browse_image, icon_color) + browse_btn = PixmapButton(browse_pix, buttons_widget) + browse_btn.setObjectName("ThumbnailPixmapHoverButton") + browse_btn.setToolTip("Browse...") buttons_layout = QtWidgets.QHBoxLayout(buttons_widget) - buttons_layout.setContentsMargins(3, 3, 3, 3) - buttons_layout.addStretch(1) + buttons_layout.setContentsMargins(0, 0, 0, 0) + buttons_layout.addWidget(take_screenshot_btn, 0) + buttons_layout.addWidget(paste_btn, 0) + buttons_layout.addWidget(browse_btn, 0) buttons_layout.addWidget(clear_button, 0) layout = QtWidgets.QHBoxLayout(self) @@ -327,6 +351,9 @@ class ThumbnailWidget(QtWidgets.QWidget): layout.addWidget(thumbnail_painter) clear_button.clicked.connect(self._on_clear_clicked) + take_screenshot_btn.clicked.connect(self._on_take_screenshot) + paste_btn.clicked.connect(self._on_paste_from_clipboard) + browse_btn.clicked.connect(self._on_browse_clicked) self._controller = controller self._output_dir = controller.get_thumbnail_temp_dir_path() @@ -338,9 +365,16 @@ class ThumbnailWidget(QtWidgets.QWidget): self._adapted_to_size = True self._last_width = None self._last_height = None + self._hide_on_finish = False self._buttons_widget = buttons_widget self._thumbnail_painter = thumbnail_painter + self._clear_button = clear_button + self._take_screenshot_btn = take_screenshot_btn + self._paste_btn = paste_btn + self._browse_btn = browse_btn + + clear_button.setEnabled(False) @property def width_ratio(self): @@ -430,13 +464,75 @@ class ThumbnailWidget(QtWidgets.QWidget): self._thumbnail_painter.clear_cache() + def _set_current_thumbails(self, thumbnail_paths): + self._thumbnail_painter.set_current_thumbnails(thumbnail_paths) + self._update_buttons_position() + def set_current_thumbnails(self, thumbnail_paths=None): self._thumbnail_painter.set_current_thumbnails(thumbnail_paths) self._update_buttons_position() + self._clear_button.setEnabled(self._thumbnail_painter.has_pixes) def _on_clear_clicked(self): self.set_current_thumbnails() self.thumbnail_cleared.emit() + self._clear_button.setEnabled(False) + + def _on_take_screenshot(self): + window = self.window() + state = window.windowState() + window.setWindowState(QtCore.Qt.WindowMinimized) + output_path = os.path.join( + self._output_dir, uuid.uuid4().hex + ".png") + if capture_to_file(output_path): + self.thumbnail_created.emit(output_path) + # restore original window state + window.setWindowState(state) + + def _on_paste_from_clipboard(self): + """Set thumbnail from a pixmap image in the system clipboard""" + + clipboard = QtWidgets.QApplication.clipboard() + pixmap = clipboard.pixmap() + if pixmap.isNull(): + return + + # Save as temporary file + output_path = os.path.join( + self._output_dir, uuid.uuid4().hex + ".png") + + output_dir = os.path.dirname(output_path) + if not os.path.exists(output_dir): + os.makedirs(output_dir) + + if pixmap.save(output_path): + self.thumbnail_created.emit(output_path) + + def _on_browse_clicked(self): + ext_filter = "Source (*{0})".format( + " *".join(self._review_extensions) + ) + filepath, _ = QtWidgets.QFileDialog.getOpenFileName( + self, "Choose thumbnail", os.path.expanduser("~"), ext_filter + ) + if not filepath: + return + valid_path = False + ext = os.path.splitext(filepath)[-1].lower() + if ext in self._review_extensions: + valid_path = True + + output = None + if valid_path: + output = export_thumbnail(filepath, self._output_dir) + + if output: + self.thumbnail_created.emit(output) + else: + self._controller.emit_card_message( + "Couldn't convert the source for thumbnail", + CardMessageTypes.error + ) def _adapt_to_size(self): if not self._adapted_to_size: @@ -452,13 +548,25 @@ class ThumbnailWidget(QtWidgets.QWidget): self._thumbnail_painter.clear_cache() def _update_buttons_position(self): - self._buttons_widget.setVisible(self._thumbnail_painter.has_pixes) size = self.size() + my_width = size.width() my_height = size.height() - height = self._buttons_widget.sizeHint().height() + buttons_sh = self._buttons_widget.sizeHint() + buttons_height = buttons_sh.height() + buttons_width = buttons_sh.width() + pos_x = my_width - (buttons_width + 3) + pos_y = my_height - (buttons_height + 3) + if pos_x < 0: + pos_x = 0 + buttons_width = my_width + if pos_y < 0: + pos_y = 0 + buttons_height = my_height self._buttons_widget.setGeometry( - 0, my_height - height, - size.width(), height + pos_x, + pos_y, + buttons_width, + buttons_height ) def resizeEvent(self, event): diff --git a/openpype/tools/utils/widgets.py b/openpype/tools/utils/widgets.py index 5a8104611b..a70437cc65 100644 --- a/openpype/tools/utils/widgets.py +++ b/openpype/tools/utils/widgets.py @@ -410,6 +410,18 @@ class PixmapButtonPainter(QtWidgets.QWidget): self._pixmap = pixmap self._cached_pixmap = None + self._disabled = False + + def resizeEvent(self, event): + super(PixmapButtonPainter, self).resizeEvent(event) + self._cached_pixmap = None + self.repaint() + + def set_enabled(self, enabled): + if self._disabled != enabled: + return + self._disabled = not enabled + self.repaint() def set_pixmap(self, pixmap): self._pixmap = pixmap @@ -444,6 +456,8 @@ class PixmapButtonPainter(QtWidgets.QWidget): if self._cached_pixmap is None: self._cache_pixmap() + if self._disabled: + painter.setOpacity(0.5) painter.drawPixmap(0, 0, self._cached_pixmap) painter.end() @@ -464,6 +478,10 @@ class PixmapButton(ClickableFrame): layout.setContentsMargins(*args) self._update_painter_geo() + def setEnabled(self, enabled): + self._button_painter.set_enabled(enabled) + super(PixmapButton, self).setEnabled(enabled) + def set_pixmap(self, pixmap): self._button_painter.set_pixmap(pixmap) From bd9a79427421c664021bc296baa852b698800269 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A0=20Serra=20Arrizabalaga?= Date: Thu, 17 Aug 2023 10:41:34 +0200 Subject: [PATCH 125/327] Fix typo on deadline OP plugin name (#5453) --- openpype/modules/deadline/plugins/publish/submit_publish_job.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 5e8c005d07..da96b429ce 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -211,7 +211,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, environment["OPENPYPE_PUBLISH_JOB"] = "1" environment["OPENPYPE_RENDER_JOB"] = "0" environment["OPENPYPE_REMOTE_PUBLISH"] = "0" - deadline_plugin = "Openpype" + deadline_plugin = "OpenPype" # Add OpenPype version if we are running from build. if is_running_from_build(): self.environ_keys.append("OPENPYPE_VERSION") From 6ae58875b5a9e0dde4a045e248e821a61997349b Mon Sep 17 00:00:00 2001 From: Kayla Man <64118225+moonyuet@users.noreply.github.com> Date: Thu, 17 Aug 2023 16:57:00 +0800 Subject: [PATCH 126/327] 3dsMax: Settings for Ayon (#5388) * 3dsmax settings for ayon * lower version to '0.1.0' * remove arguments from max application settings * RenderSettings instead of render_settings for max --------- Co-authored-by: Jakub Trllo --- .../applications/server/applications.json | 4 +- server_addon/max/server/__init__.py | 17 ++++++ server_addon/max/server/settings/__init__.py | 10 ++++ server_addon/max/server/settings/imageio.py | 48 +++++++++++++++ server_addon/max/server/settings/main.py | 60 +++++++++++++++++++ .../max/server/settings/publishers.py | 26 ++++++++ .../max/server/settings/render_settings.py | 49 +++++++++++++++ server_addon/max/server/version.py | 1 + 8 files changed, 212 insertions(+), 3 deletions(-) create mode 100644 server_addon/max/server/__init__.py create mode 100644 server_addon/max/server/settings/__init__.py create mode 100644 server_addon/max/server/settings/imageio.py create mode 100644 server_addon/max/server/settings/main.py create mode 100644 server_addon/max/server/settings/publishers.py create mode 100644 server_addon/max/server/settings/render_settings.py create mode 100644 server_addon/max/server/version.py diff --git a/server_addon/applications/server/applications.json b/server_addon/applications/server/applications.json index b19308ee7c..8e5b28623e 100644 --- a/server_addon/applications/server/applications.json +++ b/server_addon/applications/server/applications.json @@ -127,9 +127,7 @@ "linux": [] }, "arguments": { - "windows": [ - "-U MAXScript {OPENPYPE_ROOT}\\openpype\\hosts\\max\\startup\\startup.ms" - ], + "windows": [], "darwin": [], "linux": [] }, diff --git a/server_addon/max/server/__init__.py b/server_addon/max/server/__init__.py new file mode 100644 index 0000000000..31c694a084 --- /dev/null +++ b/server_addon/max/server/__init__.py @@ -0,0 +1,17 @@ +from typing import Type + +from ayon_server.addons import BaseServerAddon + +from .version import __version__ +from .settings import MaxSettings, DEFAULT_VALUES + + +class MaxAddon(BaseServerAddon): + name = "max" + title = "Max" + version = __version__ + settings_model: Type[MaxSettings] = MaxSettings + + async def get_default_settings(self): + settings_model_cls = self.get_settings_model() + return settings_model_cls(**DEFAULT_VALUES) diff --git a/server_addon/max/server/settings/__init__.py b/server_addon/max/server/settings/__init__.py new file mode 100644 index 0000000000..986b1903a5 --- /dev/null +++ b/server_addon/max/server/settings/__init__.py @@ -0,0 +1,10 @@ +from .main import ( + MaxSettings, + DEFAULT_VALUES, +) + + +__all__ = ( + "MaxSettings", + "DEFAULT_VALUES", +) diff --git a/server_addon/max/server/settings/imageio.py b/server_addon/max/server/settings/imageio.py new file mode 100644 index 0000000000..5e46104fa7 --- /dev/null +++ b/server_addon/max/server/settings/imageio.py @@ -0,0 +1,48 @@ +from pydantic import Field, validator +from ayon_server.settings import BaseSettingsModel +from ayon_server.settings.validators import ensure_unique_names + + +class ImageIOConfigModel(BaseSettingsModel): + override_global_config: bool = Field( + False, + title="Override global OCIO config" + ) + filepath: list[str] = Field( + default_factory=list, + title="Config path" + ) + + +class ImageIOFileRuleModel(BaseSettingsModel): + name: str = Field("", title="Rule name") + pattern: str = Field("", title="Regex pattern") + colorspace: str = Field("", title="Colorspace name") + ext: str = Field("", title="File extension") + + +class ImageIOFileRulesModel(BaseSettingsModel): + activate_host_rules: bool = Field(False) + rules: list[ImageIOFileRuleModel] = Field( + default_factory=list, + title="Rules" + ) + + @validator("rules") + def validate_unique_outputs(cls, value): + ensure_unique_names(value) + return value + + +class ImageIOSettings(BaseSettingsModel): + activate_host_color_management: bool = Field( + True, title="Enable Color Management" + ) + ocio_config: ImageIOConfigModel = Field( + default_factory=ImageIOConfigModel, + title="OCIO config" + ) + file_rules: ImageIOFileRulesModel = Field( + default_factory=ImageIOFileRulesModel, + title="File Rules" + ) diff --git a/server_addon/max/server/settings/main.py b/server_addon/max/server/settings/main.py new file mode 100644 index 0000000000..7f4561cbb1 --- /dev/null +++ b/server_addon/max/server/settings/main.py @@ -0,0 +1,60 @@ +from pydantic import Field +from ayon_server.settings import BaseSettingsModel +from .imageio import ImageIOSettings +from .render_settings import ( + RenderSettingsModel, DEFAULT_RENDER_SETTINGS +) +from .publishers import ( + PublishersModel, DEFAULT_PUBLISH_SETTINGS +) + + +class PRTAttributesModel(BaseSettingsModel): + _layout = "compact" + name: str = Field(title="Name") + value: str = Field(title="Attribute") + + +class PointCloudSettings(BaseSettingsModel): + attribute: list[PRTAttributesModel] = Field( + default_factory=list, title="Channel Attribute") + + +class MaxSettings(BaseSettingsModel): + imageio: ImageIOSettings = Field( + default_factory=ImageIOSettings, + title="Color Management (ImageIO)" + ) + RenderSettings: RenderSettingsModel = Field( + default_factory=RenderSettingsModel, + title="Render Settings" + ) + PointCloud: PointCloudSettings = Field( + default_factory=PointCloudSettings, + title="Point Cloud" + ) + publish: PublishersModel = Field( + default_factory=PublishersModel, + title="Publish Plugins") + + +DEFAULT_VALUES = { + "RenderSettings": DEFAULT_RENDER_SETTINGS, + "PointCloud": { + "attribute": [ + {"name": "Age", "value": "age"}, + {"name": "Radius", "value": "radius"}, + {"name": "Position", "value": "position"}, + {"name": "Rotation", "value": "rotation"}, + {"name": "Scale", "value": "scale"}, + {"name": "Velocity", "value": "velocity"}, + {"name": "Color", "value": "color"}, + {"name": "TextureCoordinate", "value": "texcoord"}, + {"name": "MaterialID", "value": "matid"}, + {"name": "custFloats", "value": "custFloats"}, + {"name": "custVecs", "value": "custVecs"}, + ] + }, + "publish": DEFAULT_PUBLISH_SETTINGS + +} diff --git a/server_addon/max/server/settings/publishers.py b/server_addon/max/server/settings/publishers.py new file mode 100644 index 0000000000..a695b85e89 --- /dev/null +++ b/server_addon/max/server/settings/publishers.py @@ -0,0 +1,26 @@ +from pydantic import Field + +from ayon_server.settings import BaseSettingsModel + + +class BasicValidateModel(BaseSettingsModel): + enabled: bool = Field(title="Enabled") + optional: bool = Field(title="Optional") + active: bool = Field(title="Active") + + +class PublishersModel(BaseSettingsModel): + ValidateFrameRange: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Frame Range", + section="Validators" + ) + + +DEFAULT_PUBLISH_SETTINGS = { + "ValidateFrameRange": { + "enabled": True, + "optional": True, + "active": True + } +} diff --git a/server_addon/max/server/settings/render_settings.py b/server_addon/max/server/settings/render_settings.py new file mode 100644 index 0000000000..6c236d9f12 --- /dev/null +++ b/server_addon/max/server/settings/render_settings.py @@ -0,0 +1,49 @@ +from pydantic import Field + +from ayon_server.settings import BaseSettingsModel + + +def aov_separators_enum(): + return [ + {"value": "dash", "label": "- (dash)"}, + {"value": "underscore", "label": "_ (underscore)"}, + {"value": "dot", "label": ". (dot)"} + ] + + +def image_format_enum(): + """Return enumerator for image output formats.""" + return [ + {"label": "bmp", "value": "bmp"}, + {"label": "exr", "value": "exr"}, + {"label": "tif", "value": "tif"}, + {"label": "tiff", "value": "tiff"}, + {"label": "jpg", "value": "jpg"}, + {"label": "png", "value": "png"}, + {"label": "tga", "value": "tga"}, + {"label": "dds", "value": "dds"} + ] + + +class RenderSettingsModel(BaseSettingsModel): + default_render_image_folder: str = Field( + title="Default render image folder" + ) + aov_separator: str = Field( + "underscore", + title="AOV Separator character", + enum_resolver=aov_separators_enum + ) + image_format: str = Field( + enum_resolver=image_format_enum, + title="Output Image Format" + ) + multipass: bool = Field(title="multipass") + + +DEFAULT_RENDER_SETTINGS = { + "default_render_image_folder": "renders/3dsmax", + "aov_separator": "underscore", + "image_format": "png", + "multipass": True +} diff --git a/server_addon/max/server/version.py b/server_addon/max/server/version.py new file mode 100644 index 0000000000..3dc1f76bc6 --- /dev/null +++ b/server_addon/max/server/version.py @@ -0,0 +1 @@ +__version__ = "0.1.0" From ecf16356378ee6daf4f2abcd771144df7b4990d1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 17 Aug 2023 12:46:57 +0200 Subject: [PATCH 127/327] updated ayon api to '0.3.5' (#5460) --- .../vendor/python/common/ayon_api/__init__.py | 22 + .../vendor/python/common/ayon_api/_api.py | 62 ++ .../python/common/ayon_api/constants.py | 19 + .../python/common/ayon_api/entity_hub.py | 748 +++++++++++++++++- .../python/common/ayon_api/graphql_queries.py | 25 + .../python/common/ayon_api/operations.py | 117 ++- .../python/common/ayon_api/server_api.py | 416 +++++++--- .../python/common/ayon_api/thumbnails.py | 219 ----- .../vendor/python/common/ayon_api/utils.py | 39 + .../vendor/python/common/ayon_api/version.py | 2 +- 10 files changed, 1320 insertions(+), 349 deletions(-) delete mode 100644 openpype/vendor/python/common/ayon_api/thumbnails.py diff --git a/openpype/vendor/python/common/ayon_api/__init__.py b/openpype/vendor/python/common/ayon_api/__init__.py index 0540d7692d..027e7a3da2 100644 --- a/openpype/vendor/python/common/ayon_api/__init__.py +++ b/openpype/vendor/python/common/ayon_api/__init__.py @@ -30,6 +30,8 @@ from ._api import ( set_client_version, get_default_settings_variant, set_default_settings_variant, + get_sender, + set_sender, get_base_url, get_rest_url, @@ -92,6 +94,7 @@ from ._api import ( get_users, get_attributes_for_type, + get_attributes_fields_for_type, get_default_fields_for_type, get_project_anatomy_preset, @@ -110,6 +113,11 @@ from ._api import ( get_addons_project_settings, get_addons_settings, + get_secrets, + get_secret, + save_secret, + delete_secret, + get_project_names, get_projects, get_project, @@ -124,6 +132,8 @@ from ._api import ( get_folders_hierarchy, get_tasks, + get_task_by_id, + get_task_by_name, get_folder_ids_with_products, get_product_by_id, @@ -154,6 +164,7 @@ from ._api import ( get_workfile_info, get_workfile_info_by_id, + get_thumbnail_by_id, get_thumbnail, get_folder_thumbnail, get_version_thumbnail, @@ -216,6 +227,8 @@ __all__ = ( "set_client_version", "get_default_settings_variant", "set_default_settings_variant", + "get_sender", + "set_sender", "get_base_url", "get_rest_url", @@ -278,6 +291,7 @@ __all__ = ( "get_users", "get_attributes_for_type", + "get_attributes_fields_for_type", "get_default_fields_for_type", "get_project_anatomy_preset", @@ -295,6 +309,11 @@ __all__ = ( "get_addons_project_settings", "get_addons_settings", + "get_secrets", + "get_secret", + "save_secret", + "delete_secret", + "get_project_names", "get_projects", "get_project", @@ -308,6 +327,8 @@ __all__ = ( "get_folders", "get_tasks", + "get_task_by_id", + "get_task_by_name", "get_folder_ids_with_products", "get_product_by_id", @@ -338,6 +359,7 @@ __all__ = ( "get_workfile_info", "get_workfile_info_by_id", + "get_thumbnail_by_id", "get_thumbnail", "get_folder_thumbnail", "get_version_thumbnail", diff --git a/openpype/vendor/python/common/ayon_api/_api.py b/openpype/vendor/python/common/ayon_api/_api.py index 26a4b1530a..1d7b1837f1 100644 --- a/openpype/vendor/python/common/ayon_api/_api.py +++ b/openpype/vendor/python/common/ayon_api/_api.py @@ -392,6 +392,28 @@ def set_default_settings_variant(variant): return con.set_default_settings_variant(variant) +def get_sender(): + """Sender used to send requests. + + Returns: + Union[str, None]: Sender name or None. + """ + + con = get_server_api_connection() + return con.get_sender() + + +def set_sender(sender): + """Change sender used for requests. + + Args: + sender (Union[str, None]): Sender name or None. + """ + + con = get_server_api_connection() + return con.set_sender(sender) + + def get_base_url(): con = get_server_api_connection() return con.get_base_url() @@ -704,6 +726,26 @@ def get_addons_settings(*args, **kwargs): return con.get_addons_settings(*args, **kwargs) +def get_secrets(*args, **kwargs): + con = get_server_api_connection() + return con.get_secrets(*args, **kwargs) + + +def get_secret(*args, **kwargs): + con = get_server_api_connection() + return con.delete_secret(*args, **kwargs) + + +def save_secret(*args, **kwargs): + con = get_server_api_connection() + return con.delete_secret(*args, **kwargs) + + +def delete_secret(*args, **kwargs): + con = get_server_api_connection() + return con.delete_secret(*args, **kwargs) + + def get_project_names(*args, **kwargs): con = get_server_api_connection() return con.get_project_names(*args, **kwargs) @@ -734,6 +776,16 @@ def get_tasks(*args, **kwargs): return con.get_tasks(*args, **kwargs) +def get_task_by_id(*args, **kwargs): + con = get_server_api_connection() + return con.get_task_by_id(*args, **kwargs) + + +def get_task_by_name(*args, **kwargs): + con = get_server_api_connection() + return con.get_task_by_name(*args, **kwargs) + + def get_folder_by_id(*args, **kwargs): con = get_server_api_connection() return con.get_folder_by_id(*args, **kwargs) @@ -904,6 +956,11 @@ def delete_project(project_name): return con.delete_project(project_name) +def get_thumbnail_by_id(project_name, thumbnail_id): + con = get_server_api_connection() + con.get_thumbnail_by_id(project_name, thumbnail_id) + + def get_thumbnail(project_name, entity_type, entity_id, thumbnail_id=None): con = get_server_api_connection() con.get_thumbnail(project_name, entity_type, entity_id, thumbnail_id) @@ -934,6 +991,11 @@ def update_thumbnail(project_name, thumbnail_id, src_filepath): return con.update_thumbnail(project_name, thumbnail_id, src_filepath) +def get_attributes_fields_for_type(entity_type): + con = get_server_api_connection() + return con.get_attributes_fields_for_type(entity_type) + + def get_default_fields_for_type(entity_type): con = get_server_api_connection() return con.get_default_fields_for_type(entity_type) diff --git a/openpype/vendor/python/common/ayon_api/constants.py b/openpype/vendor/python/common/ayon_api/constants.py index e2b05a5cae..eb1ace0590 100644 --- a/openpype/vendor/python/common/ayon_api/constants.py +++ b/openpype/vendor/python/common/ayon_api/constants.py @@ -4,6 +4,25 @@ SERVER_API_ENV_KEY = "AYON_API_KEY" # Backwards compatibility SERVER_TOKEN_ENV_KEY = SERVER_API_ENV_KEY +# --- User --- +DEFAULT_USER_FIELDS = { + "roles", + "name", + "isService", + "isManager", + "isGuest", + "isAdmin", + "defaultRoles", + "createdAt", + "active", + "hasPassword", + "updatedAt", + "apiKeyPreview", + "attrib.avatarUrl", + "attrib.email", + "attrib.fullName", +} + # --- Product types --- DEFAULT_PRODUCT_TYPE_FIELDS = { "name", diff --git a/openpype/vendor/python/common/ayon_api/entity_hub.py b/openpype/vendor/python/common/ayon_api/entity_hub.py index ab1e2584d7..b9b017bac5 100644 --- a/openpype/vendor/python/common/ayon_api/entity_hub.py +++ b/openpype/vendor/python/common/ayon_api/entity_hub.py @@ -1,10 +1,11 @@ +import re import copy import collections from abc import ABCMeta, abstractmethod import six from ._api import get_server_api_connection -from .utils import create_entity_id, convert_entity_id +from .utils import create_entity_id, convert_entity_id, slugify_string UNKNOWN_VALUE = object() PROJECT_PARENT_ID = object() @@ -545,6 +546,7 @@ class EntityHub(object): library=project["library"], folder_types=project["folderTypes"], task_types=project["taskTypes"], + statuses=project["statuses"], name=project["name"], attribs=project["ownAttrib"], data=project["data"], @@ -775,8 +777,7 @@ class EntityHub(object): "projects/{}".format(self.project_name), **project_changes ) - if response.status_code != 204: - raise ValueError("Failed to update project") + response.raise_for_status() self.project_entity.lock() @@ -1485,6 +1486,722 @@ class BaseEntity(object): self._children_ids = set(children_ids) +class ProjectStatus: + """Project status class. + + Args: + name (str): Name of the status. e.g. 'In progress' + short_name (Optional[str]): Short name of the status. e.g. 'IP' + state (Optional[Literal[not_started, in_progress, done, blocked]]): A + state of the status. + icon (Optional[str]): Icon of the status. e.g. 'play_arrow'. + color (Optional[str]): Color of the status. e.g. '#eeeeee'. + index (Optional[int]): Index of the status. + project_statuses (Optional[_ProjectStatuses]): Project statuses + wrapper. + """ + + valid_states = ("not_started", "in_progress", "done", "blocked") + color_regex = re.compile(r"#([a-f0-9]{6})$") + default_state = "in_progress" + default_color = "#eeeeee" + + def __init__( + self, + name, + short_name=None, + state=None, + icon=None, + color=None, + index=None, + project_statuses=None, + is_new=None, + ): + short_name = short_name or "" + icon = icon or "" + state = state or self.default_state + color = color or self.default_color + self._name = name + self._short_name = short_name + self._icon = icon + self._slugified_name = None + self._state = None + self._color = None + self.set_state(state) + self.set_color(color) + + self._original_name = name + self._original_short_name = short_name + self._original_icon = icon + self._original_state = state + self._original_color = color + self._original_index = index + + self._index = index + self._project_statuses = project_statuses + if is_new is None: + is_new = index is None or project_statuses is None + self._is_new = is_new + + def __str__(self): + short_name = "" + if self.short_name: + short_name = "({})".format(self.short_name) + return "<{} {}{}>".format( + self.__class__.__name__, self.name, short_name + ) + + def __repr__(self): + return str(self) + + def __getitem__(self, key): + if key in { + "name", "short_name", "icon", "state", "color", "slugified_name" + }: + return getattr(self, key) + raise KeyError(key) + + def __setitem__(self, key, value): + if key in {"name", "short_name", "icon", "state", "color"}: + return setattr(self, key, value) + raise KeyError(key) + + def lock(self): + """Lock status. + + Changes were commited and current values are now the original values. + """ + + self._is_new = False + self._original_name = self.name + self._original_short_name = self.short_name + self._original_icon = self.icon + self._original_state = self.state + self._original_color = self.color + self._original_index = self.index + + @staticmethod + def slugify_name(name): + """Slugify status name for name comparison. + + Args: + name (str): Name of the status. + + Returns: + str: Slugified name. + """ + + return slugify_string(name.lower()) + + def get_project_statuses(self): + """Internal logic method. + + Returns: + _ProjectStatuses: Project statuses object. + """ + + return self._project_statuses + + def set_project_statuses(self, project_statuses): + """Internal logic method to change parent object. + + Args: + project_statuses (_ProjectStatuses): Project statuses object. + """ + + self._project_statuses = project_statuses + + def unset_project_statuses(self, project_statuses): + """Internal logic method to unset parent object. + + Args: + project_statuses (_ProjectStatuses): Project statuses object. + """ + + if self._project_statuses is project_statuses: + self._project_statuses = None + self._index = None + + @property + def changed(self): + """Status has changed. + + Returns: + bool: Status has changed. + """ + + return ( + self._is_new + or self._original_name != self._name + or self._original_short_name != self._short_name + or self._original_index != self._index + or self._original_state != self._state + or self._original_icon != self._icon + or self._original_color != self._color + ) + + def delete(self): + """Remove status from project statuses object.""" + + if self._project_statuses is not None: + self._project_statuses.remove(self) + + def get_index(self): + """Get index of status. + + Returns: + Union[int, None]: Index of status or None if status is not under + project. + """ + + return self._index + + def set_index(self, index, **kwargs): + """Change status index. + + Returns: + Union[int, None]: Index of status or None if status is not under + project. + """ + + if kwargs.get("from_parent"): + self._index = index + else: + self._project_statuses.set_status_index(self, index) + + def get_name(self): + """Status name. + + Returns: + str: Status name. + """ + + return self._name + + def set_name(self, name): + """Change status name. + + Args: + name (str): New status name. + """ + + if not isinstance(name, six.string_types): + raise TypeError("Name must be a string.") + if name == self._name: + return + self._name = name + self._slugified_name = None + + def get_short_name(self): + """Status short name 3 letters tops. + + Returns: + str: Status short name. + """ + + return self._short_name + + def set_short_name(self, short_name): + """Change status short name. + + Args: + short_name (str): New status short name. 3 letters tops. + """ + + if not isinstance(short_name, six.string_types): + raise TypeError("Short name must be a string.") + self._short_name = short_name + + def get_icon(self): + """Name of icon to use for status. + + Returns: + str: Name of the icon. + """ + + return self._icon + + def set_icon(self, icon): + """Change status icon name. + + Args: + icon (str): Name of the icon. + """ + + if icon is None: + icon = "" + if not isinstance(icon, six.string_types): + raise TypeError("Icon name must be a string.") + self._icon = icon + + @property + def slugified_name(self): + """Slugified and lowere status name. + + Can be used for comparison of existing statuses. e.g. 'In Progress' + vs. 'in-progress'. + + Returns: + str: Slugified and lower status name. + """ + + if self._slugified_name is None: + self._slugified_name = self.slugify_name(self.name) + return self._slugified_name + + def get_state(self): + """Get state of project status. + + Return: + Literal[not_started, in_progress, done, blocked]: General + state of status. + """ + + return self._state + + def set_state(self, state): + """Set color of project status. + + Args: + state (Literal[not_started, in_progress, done, blocked]): General + state of status. + """ + + if state not in self.valid_states: + raise ValueError("Invalid state '{}'".format(str(state))) + self._state = state + + def get_color(self): + """Get color of project status. + + Returns: + str: Status color. + """ + + return self._color + + def set_color(self, color): + """Set color of project status. + + Args: + color (str): Color in hex format. Example: '#ff0000'. + """ + + if not isinstance(color, six.string_types): + raise TypeError( + "Color must be string got '{}'".format(type(color))) + color = color.lower() + if self.color_regex.fullmatch(color) is None: + raise ValueError("Invalid color value '{}'".format(color)) + self._color = color + + name = property(get_name, set_name) + short_name = property(get_short_name, set_short_name) + project_statuses = property(get_project_statuses, set_project_statuses) + index = property(get_index, set_index) + state = property(get_state, set_state) + color = property(get_color, set_color) + icon = property(get_icon, set_icon) + + def _validate_other_p_statuses(self, other): + """Validate if other status can be used for move. + + To be able to work with other status, and position them in relation, + they must belong to same existing object of '_ProjectStatuses'. + + Args: + other (ProjectStatus): Other status to validate. + """ + + o_project_statuses = other.project_statuses + m_project_statuses = self.project_statuses + if o_project_statuses is None and m_project_statuses is None: + raise ValueError("Both statuses are not assigned to a project.") + + missing_status = None + if o_project_statuses is None: + missing_status = other + elif m_project_statuses is None: + missing_status = self + if missing_status is not None: + raise ValueError( + "Status '{}' is not assigned to a project.".format( + missing_status.name)) + if m_project_statuses is not o_project_statuses: + raise ValueError( + "Statuse are assigned to different projects." + " Cannot execute move." + ) + + def move_before(self, other): + """Move status before other status. + + Args: + other (ProjectStatus): Status to move before. + """ + + self._validate_other_p_statuses(other) + self._project_statuses.set_status_index(self, other.index) + + def move_after(self, other): + """Move status after other status. + + Args: + other (ProjectStatus): Status to move after. + """ + + self._validate_other_p_statuses(other) + self._project_statuses.set_status_index(self, other.index + 1) + + def to_data(self): + """Convert status to data. + + Returns: + dict[str, str]: Status data. + """ + + output = { + "name": self.name, + "shortName": self.short_name, + "state": self.state, + "icon": self.icon, + "color": self.color, + } + if ( + not self._is_new + and self._original_name + and self.name != self._original_name + ): + output["original_name"] = self._original_name + return output + + @classmethod + def from_data(cls, data, index=None, project_statuses=None): + """Create project status from data. + + Args: + data (dict[str, str]): Status data. + index (Optional[int]): Status index. + project_statuses (Optional[ProjectStatuses]): Project statuses + object which wraps the status for a project. + """ + + return cls( + data["name"], + data.get("shortName", data.get("short_name")), + data.get("state"), + data.get("icon"), + data.get("color"), + index=index, + project_statuses=project_statuses + ) + + +class _ProjectStatuses: + """Wrapper for project statuses. + + Supports basic methods to add, change or remove statuses from a project. + + To add new statuses use 'create' or 'add_status' methods. To change + statuses receive them by one of the getter methods and change their + values. + + Todos: + Validate if statuses are duplicated. + """ + + def __init__(self, statuses): + self._statuses = [ + ProjectStatus.from_data(status, idx, self) + for idx, status in enumerate(statuses) + ] + self._orig_status_length = len(self._statuses) + self._set_called = False + + def __len__(self): + return len(self._statuses) + + def __iter__(self): + """Iterate over statuses. + + Yields: + ProjectStatus: Project status. + """ + + for status in self._statuses: + yield status + + def create( + self, + name, + short_name=None, + state=None, + icon=None, + color=None, + ): + """Create project status. + + Args: + name (str): Name of the status. e.g. 'In progress' + short_name (Optional[str]): Short name of the status. e.g. 'IP' + state (Optional[Literal[not_started, in_progress, done, blocked]]): A + state of the status. + icon (Optional[str]): Icon of the status. e.g. 'play_arrow'. + color (Optional[str]): Color of the status. e.g. '#eeeeee'. + + Returns: + ProjectStatus: Created project status. + """ + + status = ProjectStatus( + name, short_name, state, icon, color, is_new=True + ) + self.append(status) + return status + + def lock(self): + """Lock statuses. + + Changes were commited and current values are now the original values. + """ + + self._orig_status_length = len(self._statuses) + self._set_called = False + for status in self._statuses: + status.lock() + + def to_data(self): + """Convert to project statuses data.""" + + return [ + status.to_data() + for status in self._statuses + ] + + def set(self, statuses): + """Explicitly override statuses. + + This method does not handle if statuses changed or not. + + Args: + statuses (list[dict[str, str]]): List of statuses data. + """ + + self._set_called = True + self._statuses = [ + ProjectStatus.from_data(status, idx, self) + for idx, status in enumerate(statuses) + ] + + @property + def changed(self): + """Statuses have changed. + + Returns: + bool: True if statuses changed, False otherwise. + """ + + if self._set_called: + return True + + # Check if status length changed + # - when all statuses are removed it is a changed + if self._orig_status_length != len(self._statuses): + return True + # Go through all statuses and check if any of them changed + for status in self._statuses: + if status.changed: + return True + return False + + def get(self, name, default=None): + """Get status by name. + + Args: + name (str): Status name. + default (Any): Default value of status is not found. + + Returns: + Union[ProjectStatus, Any]: Status or default value. + """ + + return next( + ( + status + for status in self._statuses + if status.name == name + ), + default + ) + + get_status_by_name = get + + def index(self, status, **kwargs): + """Get status index. + + Args: + status (ProjectStatus): Status to get index of. + default (Optional[Any]): Default value if status is not found. + + Returns: + Union[int, Any]: Status index. + + Raises: + ValueError: If status is not found and default value is not + defined. + """ + + output = next( + ( + idx + for idx, st in enumerate(self._statuses) + if st is status + ), + None + ) + if output is not None: + return output + + if "default" in kwargs: + return kwargs["default"] + raise ValueError("Status '{}' not found".format(status.name)) + + def get_status_by_slugified_name(self, name): + """Get status by slugified name. + + Args: + name (str): Status name. Is slugified before search. + + Returns: + Union[ProjectStatus, None]: Status or None if not found. + """ + + slugified_name = ProjectStatus.slugify_name(name) + return next( + ( + status + for status in self._statuses + if status.slugified_name == slugified_name + ), + None + ) + + def remove_by_name(self, name, ignore_missing=False): + """Remove status by name. + + Args: + name (str): Status name. + ignore_missing (Optional[bool]): If True, no error is raised if + status is not found. + + Returns: + ProjectStatus: Removed status. + """ + + matching_status = self.get(name) + if matching_status is None: + if ignore_missing: + return + raise ValueError( + "Status '{}' not found in project".format(name)) + return self.remove(matching_status) + + def remove(self, status, ignore_missing=False): + """Remove status. + + Args: + status (ProjectStatus): Status to remove. + ignore_missing (Optional[bool]): If True, no error is raised if + status is not found. + + Returns: + Union[ProjectStatus, None]: Removed status. + """ + + index = self.index(status, default=None) + if index is None: + if ignore_missing: + return None + raise ValueError("Status '{}' not in project".format(status)) + + return self.pop(index) + + def pop(self, index): + """Remove status by index. + + Args: + index (int): Status index. + + Returns: + ProjectStatus: Removed status. + """ + + status = self._statuses.pop(index) + status.unset_project_statuses(self) + for st in self._statuses[index:]: + st.set_index(st.index - 1, from_parent=True) + return status + + def insert(self, index, status): + """Insert status at index. + + Args: + index (int): Status index. + status (Union[ProjectStatus, dict[str, str]]): Status to insert. + Can be either status object or status data. + + Returns: + ProjectStatus: Inserted status. + """ + + if not isinstance(status, ProjectStatus): + status = ProjectStatus.from_data(status) + + start_index = index + end_index = len(self._statuses) + 1 + matching_index = self.index(status, default=None) + if matching_index is not None: + if matching_index == index: + status.set_index(index, from_parent=True) + return + + self._statuses.pop(matching_index) + if matching_index < index: + start_index = matching_index + end_index = index + 1 + else: + end_index -= 1 + + status.set_project_statuses(self) + self._statuses.insert(index, status) + for idx, st in enumerate(self._statuses[start_index:end_index]): + st.set_index(start_index + idx, from_parent=True) + return status + + def append(self, status): + """Add new status to the end of the list. + + Args: + status (Union[ProjectStatus, dict[str, str]]): Status to insert. + Can be either status object or status data. + + Returns: + ProjectStatus: Inserted status. + """ + + return self.insert(len(self._statuses), status) + + def set_status_index(self, status, index): + """Set status index. + + Args: + status (ProjectStatus): Status to set index. + index (int): New status index. + """ + + return self.insert(index, status) + + class ProjectEntity(BaseEntity): """Entity representing project on AYON server. @@ -1514,7 +2231,14 @@ class ProjectEntity(BaseEntity): default_task_type_icon = "task_alt" def __init__( - self, project_code, library, folder_types, task_types, *args, **kwargs + self, + project_code, + library, + folder_types, + task_types, + statuses, + *args, + **kwargs ): super(ProjectEntity, self).__init__(*args, **kwargs) @@ -1522,11 +2246,13 @@ class ProjectEntity(BaseEntity): self._library_project = library self._folder_types = folder_types self._task_types = task_types + self._statuses_obj = _ProjectStatuses(statuses) self._orig_project_code = project_code self._orig_library_project = library self._orig_folder_types = copy.deepcopy(folder_types) self._orig_task_types = copy.deepcopy(task_types) + self._orig_statuses = copy.deepcopy(statuses) def _prepare_entity_id(self, entity_id): if entity_id != self.project_name: @@ -1573,13 +2299,24 @@ class ProjectEntity(BaseEntity): new_task_types.append(task_type) self._task_types = new_task_types + def get_orig_statuses(self): + return copy.deepcopy(self._orig_statuses) + + def get_statuses(self): + return self._statuses_obj + + def set_statuses(self, statuses): + self._statuses_obj.set(statuses) + folder_types = property(get_folder_types, set_folder_types) task_types = property(get_task_types, set_task_types) + statuses = property(get_statuses, set_statuses) def lock(self): super(ProjectEntity, self).lock() self._orig_folder_types = copy.deepcopy(self._folder_types) self._orig_task_types = copy.deepcopy(self._task_types) + self._statuses_obj.lock() @property def changes(self): @@ -1590,6 +2327,9 @@ class ProjectEntity(BaseEntity): if self._orig_task_types != self._task_types: changes["taskTypes"] = self.get_task_types() + if self._statuses_obj.changed: + changes["statuses"] = self._statuses_obj.to_data() + return changes @classmethod diff --git a/openpype/vendor/python/common/ayon_api/graphql_queries.py b/openpype/vendor/python/common/ayon_api/graphql_queries.py index 4af8c53e4e..f31134a04d 100644 --- a/openpype/vendor/python/common/ayon_api/graphql_queries.py +++ b/openpype/vendor/python/common/ayon_api/graphql_queries.py @@ -462,3 +462,28 @@ def events_graphql_query(fields): for k, v in value.items(): query_queue.append((k, v, field)) return query + + +def users_graphql_query(fields): + query = GraphQlQuery("Users") + names_var = query.add_variable("userNames", "[String!]") + + users_field = query.add_field_with_edges("users") + users_field.set_filter("names", names_var) + + nested_fields = fields_to_dict(set(fields)) + + query_queue = collections.deque() + for key, value in nested_fields.items(): + query_queue.append((key, value, users_field)) + + while query_queue: + item = query_queue.popleft() + key, value, parent = item + field = parent.add_field(key) + if value is FIELD_VALUE: + continue + + for k, v in value.items(): + query_queue.append((k, v, field)) + return query diff --git a/openpype/vendor/python/common/ayon_api/operations.py b/openpype/vendor/python/common/ayon_api/operations.py index 7cf610a566..eb2ca8afe3 100644 --- a/openpype/vendor/python/common/ayon_api/operations.py +++ b/openpype/vendor/python/common/ayon_api/operations.py @@ -1,3 +1,4 @@ +import os import copy import collections import uuid @@ -22,6 +23,8 @@ def new_folder_entity( name, folder_type, parent_id=None, + status=None, + tags=None, attribs=None, data=None, thumbnail_id=None, @@ -32,12 +35,14 @@ def new_folder_entity( Args: name (str): Is considered as unique identifier of folder in project. folder_type (str): Type of folder. - parent_id (Optional[str]]): Id of parent folder. + parent_id (Optional[str]): Parent folder id. + status (Optional[str]): Product status. + tags (Optional[List[str]]): List of tags. attribs (Optional[Dict[str, Any]]): Explicitly set attributes of folder. data (Optional[Dict[str, Any]]): Custom folder data. Empty dictionary is used if not passed. - thumbnail_id (Optional[str]): Id of thumbnail related to folder. + thumbnail_id (Optional[str]): Thumbnail id related to folder. entity_id (Optional[str]): Predefined id of entity. New id is created if not passed. @@ -54,7 +59,7 @@ def new_folder_entity( if parent_id is not None: parent_id = _create_or_convert_to_id(parent_id) - return { + output = { "id": _create_or_convert_to_id(entity_id), "name": name, # This will be ignored @@ -64,6 +69,11 @@ def new_folder_entity( "attrib": attribs, "thumbnailId": thumbnail_id } + if status: + output["status"] = status + if tags: + output["tags"] = tags + return output def new_product_entity( @@ -71,6 +81,7 @@ def new_product_entity( product_type, folder_id, status=None, + tags=None, attribs=None, data=None, entity_id=None @@ -81,8 +92,9 @@ def new_product_entity( name (str): Is considered as unique identifier of product under folder. product_type (str): Product type. - folder_id (str): Id of parent folder. + folder_id (str): Parent folder id. status (Optional[str]): Product status. + tags (Optional[List[str]]): List of tags. attribs (Optional[Dict[str, Any]]): Explicitly set attributes of product. data (Optional[Dict[str, Any]]): product entity data. Empty dictionary @@ -110,6 +122,8 @@ def new_product_entity( } if status: output["status"] = status + if tags: + output["tags"] = tags return output @@ -119,6 +133,8 @@ def new_version_entity( task_id=None, thumbnail_id=None, author=None, + status=None, + tags=None, attribs=None, data=None, entity_id=None @@ -128,10 +144,12 @@ def new_version_entity( Args: version (int): Is considered as unique identifier of version under product. - product_id (str): Id of parent product. - task_id (Optional[str]]): Id of task under which product was created. - thumbnail_id (Optional[str]]): Thumbnail related to version. - author (Optional[str]]): Name of version author. + product_id (str): Parent product id. + task_id (Optional[str]): Task id under which product was created. + thumbnail_id (Optional[str]): Thumbnail related to version. + author (Optional[str]): Name of version author. + status (Optional[str]): Version status. + tags (Optional[List[str]]): List of tags. attribs (Optional[Dict[str, Any]]): Explicitly set attributes of version. data (Optional[Dict[str, Any]]): Version entity custom data. @@ -164,6 +182,10 @@ def new_version_entity( output["thumbnailId"] = thumbnail_id if author: output["author"] = author + if tags: + output["tags"] = tags + if status: + output["status"] = status return output @@ -173,6 +195,8 @@ def new_hero_version_entity( task_id=None, thumbnail_id=None, author=None, + status=None, + tags=None, attribs=None, data=None, entity_id=None @@ -182,10 +206,12 @@ def new_hero_version_entity( Args: version (int): Is considered as unique identifier of version under product. Should be same as standard version if there is any. - product_id (str): Id of parent product. - task_id (Optional[str]): Id of task under which product was created. + product_id (str): Parent product id. + task_id (Optional[str]): Task id under which product was created. thumbnail_id (Optional[str]): Thumbnail related to version. author (Optional[str]): Name of version author. + status (Optional[str]): Version status. + tags (Optional[List[str]]): List of tags. attribs (Optional[Dict[str, Any]]): Explicitly set attributes of version. data (Optional[Dict[str, Any]]): Version entity data. @@ -215,18 +241,32 @@ def new_hero_version_entity( output["thumbnailId"] = thumbnail_id if author: output["author"] = author + if tags: + output["tags"] = tags + if status: + output["status"] = status return output def new_representation_entity( - name, version_id, attribs=None, data=None, entity_id=None + name, + version_id, + files, + status=None, + tags=None, + attribs=None, + data=None, + entity_id=None ): """Create skeleton data of representation entity. Args: name (str): Representation name considered as unique identifier of representation under version. - version_id (str): Id of parent version. + version_id (str): Parent version id. + files (list[dict[str, str]]): List of files in representation. + status (Optional[str]): Representation status. + tags (Optional[List[str]]): List of tags. attribs (Optional[Dict[str, Any]]): Explicitly set attributes of representation. data (Optional[Dict[str, Any]]): Representation entity data. @@ -243,27 +283,42 @@ def new_representation_entity( if data is None: data = {} - return { + output = { "id": _create_or_convert_to_id(entity_id), "versionId": _create_or_convert_to_id(version_id), + "files": files, "name": name, "data": data, "attrib": attribs } + if tags: + output["tags"] = tags + if status: + output["status"] = status + return output -def new_workfile_info_doc( - filename, folder_id, task_name, files, data=None, entity_id=None +def new_workfile_info( + filepath, + task_id, + status=None, + tags=None, + attribs=None, + description=None, + data=None, + entity_id=None ): """Create skeleton data of workfile info entity. Workfile entity is at this moment used primarily for artist notes. Args: - filename (str): Filename of workfile. - folder_id (str): Id of folder under which workfile live. - task_name (str): Task under which was workfile created. - files (List[str]): List of rootless filepaths related to workfile. + filepath (str): Rootless workfile filepath. + task_id (str): Task under which was workfile created. + status (Optional[str]): Workfile status. + tags (Optional[List[str]]): Workfile tags. + attribs (Options[dic[str, Any]]): Explicitly set attributes. + description (Optional[str]): Workfile description. data (Optional[Dict[str, Any]]): Additional metadata. entity_id (Optional[str]): Predefined id of entity. New id is created if not passed. @@ -272,17 +327,31 @@ def new_workfile_info_doc( Dict[str, Any]: Skeleton of workfile info entity. """ + if attribs is None: + attribs = {} + + if "extension" not in attribs: + attribs["extension"] = os.path.splitext(filepath)[-1] + + if description: + attribs["description"] = description + if not data: data = {} - return { + output = { "id": _create_or_convert_to_id(entity_id), - "parent": _create_or_convert_to_id(folder_id), - "task_name": task_name, - "filename": filename, + "taskId": task_id, + "path": filepath, "data": data, - "files": files + "attrib": attribs } + if status: + output["status"] = status + + if tags: + output["tags"] = tags + return output @six.add_metaclass(ABCMeta) diff --git a/openpype/vendor/python/common/ayon_api/server_api.py b/openpype/vendor/python/common/ayon_api/server_api.py index c578124cfc..f2689e88dc 100644 --- a/openpype/vendor/python/common/ayon_api/server_api.py +++ b/openpype/vendor/python/common/ayon_api/server_api.py @@ -14,7 +14,16 @@ except ImportError: HTTPStatus = None import requests -from requests.exceptions import JSONDecodeError as RequestsJSONDecodeError +try: + # This should be used if 'requests' have it available + from requests.exceptions import JSONDecodeError as RequestsJSONDecodeError +except ImportError: + # Older versions of 'requests' don't have custom exception for json + # decode error + try: + from simplejson import JSONDecodeError as RequestsJSONDecodeError + except ImportError: + from json import JSONDecodeError as RequestsJSONDecodeError from .constants import ( DEFAULT_PRODUCT_TYPE_FIELDS, @@ -27,8 +36,8 @@ from .constants import ( REPRESENTATION_FILES_FIELDS, DEFAULT_WORKFILE_INFO_FIELDS, DEFAULT_EVENT_FIELDS, + DEFAULT_USER_FIELDS, ) -from .thumbnails import ThumbnailCache from .graphql import GraphQlQuery, INTROSPECTION_QUERY from .graphql_queries import ( project_graphql_query, @@ -43,6 +52,7 @@ from .graphql_queries import ( representations_parents_qraphql_query, workfiles_info_graphql_query, events_graphql_query, + users_graphql_query, ) from .exceptions import ( FailedOperations, @@ -61,6 +71,7 @@ from .utils import ( failed_json_default, TransferProgress, create_dependency_package_basename, + ThumbnailContent, ) PatternType = type(re.compile("")) @@ -319,6 +330,8 @@ class ServerAPI(object): default_settings_variant (Optional[Literal["production", "staging"]]): Settings variant used by default if a method for settings won't get any (by default is 'production'). + sender (Optional[str]): Sender of requests. Used in server logs and + propagated into events. ssl_verify (Union[bool, str, None]): Verify SSL certificate Looks for env variable value 'AYON_CA_FILE' by default. If not available then 'True' is used. @@ -335,6 +348,7 @@ class ServerAPI(object): site_id=None, client_version=None, default_settings_variant=None, + sender=None, ssl_verify=None, cert=None, create_session=True, @@ -354,6 +368,7 @@ class ServerAPI(object): default_settings_variant or "production" ) + self._sender = sender if ssl_verify is None: # Custom AYON env variable for CA file or 'True' @@ -390,7 +405,6 @@ class ServerAPI(object): self._entity_type_attributes_cache = {} self._as_user_stack = _AsUserStack() - self._thumbnail_cache = ThumbnailCache(True) # Create session if self._access_token and create_session: @@ -559,6 +573,29 @@ class ServerAPI(object): set_default_settings_variant ) + def get_sender(self): + """Sender used to send requests. + + Returns: + Union[str, None]: Sender name or None. + """ + + return self._sender + + def set_sender(self, sender): + """Change sender used for requests. + + Args: + sender (Union[str, None]): Sender name or None. + """ + + if sender == self._sender: + return + self._sender = sender + self._update_session_headers() + + sender = property(get_sender, set_sender) + def get_default_service_username(self): """Default username used for callbacks when used with service API key. @@ -742,6 +779,7 @@ class ServerAPI(object): ("X-as-user", self._as_user_stack.username), ("x-ayon-version", self._client_version), ("x-ayon-site-id", self._site_id), + ("x-sender", self._sender), ): if value is not None: self._session.headers[key] = value @@ -826,10 +864,36 @@ class ServerAPI(object): self._access_token_is_service = None return None - def get_users(self): - # TODO how to find out if user have permission? - users = self.get("users") - return users.data + def get_users(self, usernames=None, fields=None): + """Get Users. + + Args: + usernames (Optional[Iterable[str]]): Filter by usernames. + fields (Optional[Iterable[str]]): fields to be queried + for users. + + Returns: + Generator[dict[str, Any]]: Queried users. + """ + + filters = {} + if usernames is not None: + usernames = set(usernames) + if not usernames: + return + filters["userNames"] = list(usernames) + + if not fields: + fields = self.get_default_fields_for_type("user") + + query = users_graphql_query(set(fields)) + for attr, filter_value in filters.items(): + query.set_variable_value(attr, filter_value) + + for parsed_data in query.continuous_query(self): + for user in parsed_data["users"]: + user["roles"] = json.loads(user["roles"]) + yield user def get_user(self, username=None): output = None @@ -859,6 +923,9 @@ class ServerAPI(object): if self._client_version is not None: headers["x-ayon-version"] = self._client_version + if self._sender is not None: + headers["x-sender"] = self._sender + if self._access_token: if self._access_token_is_service: headers["X-Api-Key"] = self._access_token @@ -900,18 +967,24 @@ class ServerAPI(object): self.validate_server_availability() - response = self.post( - "auth/login", - name=username, - password=password - ) - if response.status_code != 200: - _detail = response.data.get("detail") - details = "" - if _detail: - details = " {}".format(_detail) + self._token_validation_started = True - raise AuthenticationError("Login failed {}".format(details)) + try: + response = self.post( + "auth/login", + name=username, + password=password + ) + if response.status_code != 200: + _detail = response.data.get("detail") + details = "" + if _detail: + details = " {}".format(_detail) + + raise AuthenticationError("Login failed {}".format(details)) + + finally: + self._token_validation_started = False self._access_token = response["token"] @@ -1127,7 +1200,7 @@ class ServerAPI(object): filters["includeLogsFilter"] = include_logs if not fields: - fields = DEFAULT_EVENT_FIELDS + fields = self.get_default_fields_for_type("event") query = events_graphql_query(set(fields)) for attr, filter_value in filters.items(): @@ -1228,7 +1301,8 @@ class ServerAPI(object): target_topic, sender, description=None, - sequential=None + sequential=None, + events_filter=None, ): """Enroll job based on events. @@ -1270,6 +1344,8 @@ class ServerAPI(object): in target event. sequential (Optional[bool]): The source topic must be processed in sequence. + events_filter (Optional[ayon_server.sqlfilter.Filter]): A dict-like + with conditions to filter the source event. Returns: Union[None, dict[str, Any]]: None if there is no event matching @@ -1285,6 +1361,8 @@ class ServerAPI(object): kwargs["sequential"] = sequential if description is not None: kwargs["description"] = description + if events_filter is not None: + kwargs["filter"] = events_filter response = self.post("enroll", **kwargs) if response.status_code == 204: return None @@ -1612,6 +1690,19 @@ class ServerAPI(object): return copy.deepcopy(attributes) + def get_attributes_fields_for_type(self, entity_type): + """Prepare attribute fields for entity type. + + Returns: + set[str]: Attributes fields for entity type. + """ + + attributes = self.get_attributes_for_type(entity_type) + return { + "attrib.{}".format(attr) + for attr in attributes + } + def get_default_fields_for_type(self, entity_type): """Default fields for entity type. @@ -1624,51 +1715,46 @@ class ServerAPI(object): set[str]: Fields that should be queried from server. """ - attributes = self.get_attributes_for_type(entity_type) + # Event does not have attributes + if entity_type == "event": + return set(DEFAULT_EVENT_FIELDS) + if entity_type == "project": - return DEFAULT_PROJECT_FIELDS | { - "attrib.{}".format(attr) - for attr in attributes - } + entity_type_defaults = DEFAULT_PROJECT_FIELDS - if entity_type == "folder": - return DEFAULT_FOLDER_FIELDS | { - "attrib.{}".format(attr) - for attr in attributes - } + elif entity_type == "folder": + entity_type_defaults = DEFAULT_FOLDER_FIELDS - if entity_type == "task": - return DEFAULT_TASK_FIELDS | { - "attrib.{}".format(attr) - for attr in attributes - } + elif entity_type == "task": + entity_type_defaults = DEFAULT_TASK_FIELDS - if entity_type == "product": - return DEFAULT_PRODUCT_FIELDS | { - "attrib.{}".format(attr) - for attr in attributes - } + elif entity_type == "product": + entity_type_defaults = DEFAULT_PRODUCT_FIELDS - if entity_type == "version": - return DEFAULT_VERSION_FIELDS | { - "attrib.{}".format(attr) - for attr in attributes - } + elif entity_type == "version": + entity_type_defaults = DEFAULT_VERSION_FIELDS - if entity_type == "representation": - return ( + elif entity_type == "representation": + entity_type_defaults = ( DEFAULT_REPRESENTATION_FIELDS | REPRESENTATION_FILES_FIELDS - | { - "attrib.{}".format(attr) - for attr in attributes - } ) - if entity_type == "productType": - return DEFAULT_PRODUCT_TYPE_FIELDS + elif entity_type == "productType": + entity_type_defaults = DEFAULT_PRODUCT_TYPE_FIELDS - raise ValueError("Unknown entity type \"{}\"".format(entity_type)) + elif entity_type == "workfile": + entity_type_defaults = DEFAULT_WORKFILE_INFO_FIELDS + + elif entity_type == "user": + entity_type_defaults = DEFAULT_USER_FIELDS + + else: + raise ValueError("Unknown entity type \"{}\"".format(entity_type)) + return ( + entity_type_defaults + | self.get_attributes_fields_for_type(entity_type) + ) def get_addons_info(self, details=True): """Get information about addons available on server. @@ -2926,6 +3012,79 @@ class ServerAPI(object): only_values=only_values ) + def get_secrets(self): + """Get all secrets. + + Example output: + [ + { + "name": "secret_1", + "value": "secret_value_1", + }, + { + "name": "secret_2", + "value": "secret_value_2", + } + ] + + Returns: + list[dict[str, str]]: List of secret entities. + """ + + response = self.get("secrets") + response.raise_for_status() + return response.data + + def get_secret(self, secret_name): + """Get secret by name. + + Example output: + { + "name": "secret_name", + "value": "secret_value", + } + + Args: + secret_name (str): Name of secret. + + Returns: + dict[str, str]: Secret entity data. + """ + + response = self.get("secrets/{}".format(secret_name)) + response.raise_for_status() + return response.data + + def save_secret(self, secret_name, secret_value): + """Save secret. + + This endpoint can create and update secret. + + Args: + secret_name (str): Name of secret. + secret_value (str): Value of secret. + """ + + response = self.put( + "secrets/{}".format(secret_name), + name=secret_name, + value=secret_value, + ) + response.raise_for_status() + return response.data + + + def delete_secret(self, secret_name): + """Delete secret by name. + + Args: + secret_name (str): Name of secret to delete. + """ + + response = self.delete("secrets/{}".format(secret_name)) + response.raise_for_status() + return response.data + # Entity getters def get_rest_project(self, project_name): """Query project by name. @@ -3070,8 +3229,6 @@ class ServerAPI(object): else: use_rest = False fields = set(fields) - if own_attributes: - fields.add("ownAttrib") for field in fields: if field.startswith("config"): use_rest = True @@ -3084,6 +3241,13 @@ class ServerAPI(object): yield project else: + if "attrib" in fields: + fields.remove("attrib") + fields |= self.get_attributes_fields_for_type("project") + + if own_attributes: + fields.add("ownAttrib") + query = projects_graphql_query(fields) for parsed_data in query.continuous_query(self): for project in parsed_data["projects"]: @@ -3124,8 +3288,12 @@ class ServerAPI(object): fill_own_attribs(project) return project + if "attrib" in fields: + fields.remove("attrib") + fields |= self.get_attributes_fields_for_type("project") + if own_attributes: - field.add("ownAttrib") + fields.add("ownAttrib") query = project_graphql_query(fields) query.set_variable_value("projectName", project_name) @@ -3282,10 +3450,13 @@ class ServerAPI(object): filters["parentFolderIds"] = list(parent_ids) - if fields: - fields = set(fields) - else: + if not fields: fields = self.get_default_fields_for_type("folder") + else: + fields = set(fields) + if "attrib" in fields: + fields.remove("attrib") + fields |= self.get_attributes_fields_for_type("folder") use_rest = False if "data" in fields: @@ -3519,8 +3690,11 @@ class ServerAPI(object): if not fields: fields = self.get_default_fields_for_type("task") - - fields = set(fields) + else: + fields = set(fields) + if "attrib" in fields: + fields.remove("attrib") + fields |= self.get_attributes_fields_for_type("task") use_rest = False if "data" in fields: @@ -3705,6 +3879,9 @@ class ServerAPI(object): # Convert fields and add minimum required fields if fields: fields = set(fields) | {"id"} + if "attrib" in fields: + fields.remove("attrib") + fields |= self.get_attributes_fields_for_type("folder") else: fields = self.get_default_fields_for_type("product") @@ -3961,7 +4138,11 @@ class ServerAPI(object): if not fields: fields = self.get_default_fields_for_type("version") - fields = set(fields) + else: + fields = set(fields) + if "attrib" in fields: + fields.remove("attrib") + fields |= self.get_attributes_fields_for_type("version") if active is not None: fields.add("active") @@ -4419,7 +4600,11 @@ class ServerAPI(object): if not fields: fields = self.get_default_fields_for_type("representation") - fields = set(fields) + else: + fields = set(fields) + if "attrib" in fields: + fields.remove("attrib") + fields |= self.get_attributes_fields_for_type("representation") use_rest = False if "data" in fields: @@ -4765,8 +4950,15 @@ class ServerAPI(object): filters["workfileIds"] = list(workfile_ids) if not fields: - fields = DEFAULT_WORKFILE_INFO_FIELDS + fields = self.get_default_fields_for_type("workfile") + fields = set(fields) + if "attrib" in fields: + fields.remove("attrib") + fields |= { + "attrib.{}".format(attr) + for attr in self.get_attributes_for_type("workfile") + } if own_attributes: fields.add("ownAttrib") @@ -4843,18 +5035,61 @@ class ServerAPI(object): return workfile_info return None + def _prepare_thumbnail_content(self, project_name, response): + content = None + content_type = response.content_type + + # It is expected the response contains thumbnail id otherwise the + # content cannot be cached and filepath returned + thumbnail_id = response.headers.get("X-Thumbnail-Id") + if thumbnail_id is not None: + content = response.content + + return ThumbnailContent( + project_name, thumbnail_id, content, content_type + ) + + def get_thumbnail_by_id(self, project_name, thumbnail_id): + """Get thumbnail from server by id. + + Permissions of thumbnails are related to entities so thumbnails must + be queried per entity. So an entity type and entity type is required + to be passed. + + Notes: + It is recommended to use one of prepared entity type specific + methods 'get_folder_thumbnail', 'get_version_thumbnail' or + 'get_workfile_thumbnail'. + We do recommend pass thumbnail id if you have access to it. Each + entity that allows thumbnails has 'thumbnailId' field, so it + can be queried. + + Args: + project_name (str): Project under which the entity is located. + thumbnail_id (Optional[str]): DEPRECATED Use + 'get_thumbnail_by_id'. + + Returns: + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. + """ + + response = self.raw_get( + "projects/{}/thumbnails/{}".format( + project_name, + thumbnail_id + ) + ) + return self._prepare_thumbnail_content(project_name, response) + def get_thumbnail( self, project_name, entity_type, entity_id, thumbnail_id=None ): """Get thumbnail from server. - Permissions of thumbnails are related to entities so thumbnails must be - queried per entity. So an entity type and entity type is required to - be passed. - - If thumbnail id is passed logic can look into locally cached thumbnails - before calling server which can enhance loading time. If thumbnail id - is not passed the thumbnail is always downloaded even if is available. + Permissions of thumbnails are related to entities so thumbnails must + be queried per entity. So an entity type and entity type is required + to be passed. Notes: It is recommended to use one of prepared entity type specific @@ -4868,20 +5103,16 @@ class ServerAPI(object): project_name (str): Project under which the entity is located. entity_type (str): Entity type which passed entity id represents. entity_id (str): Entity id for which thumbnail should be returned. - thumbnail_id (Optional[str]): Prepared thumbnail id from entity. - Used only to check if thumbnail was already cached. + thumbnail_id (Optional[str]): DEPRECATED Use + 'get_thumbnail_by_id'. Returns: - Union[str, None]: Path to downloaded thumbnail or none if entity - does not have any (or if user does not have permissions). + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. """ - # Look for thumbnail into cache and return the path if was found - filepath = self._thumbnail_cache.get_thumbnail_filepath( - project_name, thumbnail_id - ) - if filepath: - return filepath + if thumbnail_id: + return self.get_thumbnail_by_id(project_name, thumbnail_id) if entity_type in ( "folder", @@ -4890,29 +5121,12 @@ class ServerAPI(object): ): entity_type += "s" - # Receive thumbnail content from server - result = self.raw_get("projects/{}/{}/{}/thumbnail".format( + response = self.raw_get("projects/{}/{}/{}/thumbnail".format( project_name, entity_type, entity_id )) - - if result.content_type is None: - return None - - # It is expected the response contains thumbnail id otherwise the - # content cannot be cached and filepath returned - thumbnail_id = result.headers.get("X-Thumbnail-Id") - if thumbnail_id is None: - return None - - # Cache thumbnail and return path - return self._thumbnail_cache.store_thumbnail( - project_name, - thumbnail_id, - result.content, - result.content_type - ) + return self._prepare_thumbnail_content(project_name, response) def get_folder_thumbnail( self, project_name, folder_id, thumbnail_id=None diff --git a/openpype/vendor/python/common/ayon_api/thumbnails.py b/openpype/vendor/python/common/ayon_api/thumbnails.py deleted file mode 100644 index 50acd94dcb..0000000000 --- a/openpype/vendor/python/common/ayon_api/thumbnails.py +++ /dev/null @@ -1,219 +0,0 @@ -import os -import time -import collections - -import appdirs - -FileInfo = collections.namedtuple( - "FileInfo", - ("path", "size", "modification_time") -) - - -class ThumbnailCache: - """Cache of thumbnails on local storage. - - Thumbnails are cached to appdirs to predefined directory. Each project has - own subfolder with thumbnails -> that's because each project has own - thumbnail id validation and file names are thumbnail ids with matching - extension. Extensions are predefined (.png and .jpeg). - - Cache has cleanup mechanism which is triggered on initialized by default. - - The cleanup has 2 levels: - 1. soft cleanup which remove all files that are older then 'days_alive' - 2. max size cleanup which remove all files until the thumbnails folder - contains less then 'max_filesize' - - this is time consuming so it's not triggered automatically - - Args: - cleanup (bool): Trigger soft cleanup (Cleanup expired thumbnails). - """ - - # Lifetime of thumbnails (in seconds) - # - default 3 days - days_alive = 3 * 24 * 60 * 60 - # Max size of thumbnail directory (in bytes) - # - default 2 Gb - max_filesize = 2 * 1024 * 1024 * 1024 - - def __init__(self, cleanup=True): - self._thumbnails_dir = None - if cleanup: - self.cleanup() - - def get_thumbnails_dir(self): - """Root directory where thumbnails are stored. - - Returns: - str: Path to thumbnails root. - """ - - if self._thumbnails_dir is None: - directory = appdirs.user_data_dir("ayon", "ynput") - self._thumbnails_dir = os.path.join(directory, "thumbnails") - return self._thumbnails_dir - - thumbnails_dir = property(get_thumbnails_dir) - - def get_thumbnails_dir_file_info(self): - """Get information about all files in thumbnails directory. - - Returns: - List[FileInfo]: List of file information about all files. - """ - - thumbnails_dir = self.thumbnails_dir - files_info = [] - if not os.path.exists(thumbnails_dir): - return files_info - - for root, _, filenames in os.walk(thumbnails_dir): - for filename in filenames: - path = os.path.join(root, filename) - files_info.append(FileInfo( - path, os.path.getsize(path), os.path.getmtime(path) - )) - return files_info - - def get_thumbnails_dir_size(self, files_info=None): - """Got full size of thumbnail directory. - - Args: - files_info (List[FileInfo]): Prepared file information about - files in thumbnail directory. - - Returns: - int: File size of all files in thumbnail directory. - """ - - if files_info is None: - files_info = self.get_thumbnails_dir_file_info() - - if not files_info: - return 0 - - return sum( - file_info.size - for file_info in files_info - ) - - def cleanup(self, check_max_size=False): - """Cleanup thumbnails directory. - - Args: - check_max_size (bool): Also cleanup files to match max size of - thumbnails directory. - """ - - thumbnails_dir = self.get_thumbnails_dir() - # Skip if thumbnails dir does not exists yet - if not os.path.exists(thumbnails_dir): - return - - self._soft_cleanup(thumbnails_dir) - if check_max_size: - self._max_size_cleanup(thumbnails_dir) - - def _soft_cleanup(self, thumbnails_dir): - current_time = time.time() - for root, _, filenames in os.walk(thumbnails_dir): - for filename in filenames: - path = os.path.join(root, filename) - modification_time = os.path.getmtime(path) - if current_time - modification_time > self.days_alive: - os.remove(path) - - def _max_size_cleanup(self, thumbnails_dir): - files_info = self.get_thumbnails_dir_file_info() - size = self.get_thumbnails_dir_size(files_info) - if size < self.max_filesize: - return - - sorted_file_info = collections.deque( - sorted(files_info, key=lambda item: item.modification_time) - ) - diff = size - self.max_filesize - while diff > 0: - if not sorted_file_info: - break - - file_info = sorted_file_info.popleft() - diff -= file_info.size - os.remove(file_info.path) - - def get_thumbnail_filepath(self, project_name, thumbnail_id): - """Get thumbnail by thumbnail id. - - Args: - project_name (str): Name of project. - thumbnail_id (str): Thumbnail id. - - Returns: - Union[str, None]: Path to thumbnail image or None if thumbnail - is not cached yet. - """ - - if not thumbnail_id: - return None - - for ext in ( - ".png", - ".jpeg", - ): - filepath = os.path.join( - self.thumbnails_dir, project_name, thumbnail_id + ext - ) - if os.path.exists(filepath): - return filepath - return None - - def get_project_dir(self, project_name): - """Path to root directory for specific project. - - Args: - project_name (str): Name of project for which root directory path - should be returned. - - Returns: - str: Path to root of project's thumbnails. - """ - - return os.path.join(self.thumbnails_dir, project_name) - - def make_sure_project_dir_exists(self, project_name): - project_dir = self.get_project_dir(project_name) - if not os.path.exists(project_dir): - os.makedirs(project_dir) - return project_dir - - def store_thumbnail(self, project_name, thumbnail_id, content, mime_type): - """Store thumbnail to cache folder. - - Args: - project_name (str): Project where the thumbnail belong to. - thumbnail_id (str): Id of thumbnail. - content (bytes): Byte content of thumbnail file. - mime_data (str): Type of content. - - Returns: - str: Path to cached thumbnail image file. - """ - - if mime_type == "image/png": - ext = ".png" - elif mime_type == "image/jpeg": - ext = ".jpeg" - else: - raise ValueError( - "Unknown mime type for thumbnail \"{}\"".format(mime_type)) - - project_dir = self.make_sure_project_dir_exists(project_name) - thumbnail_path = os.path.join(project_dir, thumbnail_id + ext) - with open(thumbnail_path, "wb") as stream: - stream.write(content) - - current_time = time.time() - os.utime(thumbnail_path, (current_time, current_time)) - - return thumbnail_path diff --git a/openpype/vendor/python/common/ayon_api/utils.py b/openpype/vendor/python/common/ayon_api/utils.py index 93822a58ac..314d13faec 100644 --- a/openpype/vendor/python/common/ayon_api/utils.py +++ b/openpype/vendor/python/common/ayon_api/utils.py @@ -27,6 +27,45 @@ RepresentationParents = collections.namedtuple( ) +class ThumbnailContent: + """Wrapper for thumbnail content. + + Args: + project_name (str): Project name. + thumbnail_id (Union[str, None]): Thumbnail id. + content_type (Union[str, None]): Content type e.g. 'image/png'. + content (Union[bytes, None]): Thumbnail content. + """ + + def __init__(self, project_name, thumbnail_id, content, content_type): + self.project_name = project_name + self.thumbnail_id = thumbnail_id + self.content_type = content_type + self.content = content or b"" + + @property + def id(self): + """Wrapper for thumbnail id. + + Returns: + + """ + + return self.thumbnail_id + + @property + def is_valid(self): + """Content of thumbnail is valid. + + Returns: + bool: Content is valid and can be used. + """ + return ( + self.thumbnail_id is not None + and self.content_type is not None + ) + + def prepare_query_string(key_values): """Prepare data to query string. diff --git a/openpype/vendor/python/common/ayon_api/version.py b/openpype/vendor/python/common/ayon_api/version.py index 93024ea5f2..df841e0829 100644 --- a/openpype/vendor/python/common/ayon_api/version.py +++ b/openpype/vendor/python/common/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for Ayon server.""" -__version__ = "0.3.3" +__version__ = "0.3.5" From b2a6e16ae8a1466843fdd4958a7b49bb14adc34a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 17 Aug 2023 21:22:34 +0800 Subject: [PATCH 128/327] master container is now with the namespace --- openpype/hosts/max/api/lib.py | 58 ++++++++++++++++++- openpype/hosts/max/api/pipeline.py | 7 ++- .../hosts/max/plugins/load/load_camera_fbx.py | 9 ++- .../hosts/max/plugins/load/load_max_scene.py | 10 +++- openpype/hosts/max/plugins/load/load_model.py | 13 ++++- .../hosts/max/plugins/load/load_model_fbx.py | 8 ++- .../hosts/max/plugins/load/load_model_obj.py | 8 ++- .../hosts/max/plugins/load/load_model_usd.py | 9 ++- .../hosts/max/plugins/load/load_pointcache.py | 8 ++- .../hosts/max/plugins/load/load_pointcloud.py | 9 ++- .../max/plugins/load/load_redshift_proxy.py | 9 ++- 11 files changed, 134 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index ccd4cd67e1..b58b4f5b11 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -6,7 +6,7 @@ from typing import Any, Dict, Union import six from openpype.pipeline.context_tools import ( - get_current_project, get_current_project_asset,) + get_current_project, get_current_project_asset) from pymxs import runtime as rt JSON_PREFIX = "JSON::" @@ -312,3 +312,59 @@ def set_timeline(frameStart, frameEnd): """ rt.animationRange = rt.interval(frameStart, frameEnd) return rt.animationRange + + +def unique_namespace(namespace, format="%02d", + prefix="", suffix="", con_suffix="CON"): + from pymxs import runtime as rt + """Return unique namespace + + Arguments: + namespace (str): Name of namespace to consider + format (str, optional): Formatting of the given iteration number + suffix (str, optional): Only consider namespaces with this suffix. + con_suffix: max only, for finding the name of the master container + + >>> unique_namespace("bar") + # bar01 + >>> unique_namespace(":hello") + # :hello01 + >>> unique_namespace("bar:", suffix="_NS") + # bar01_NS: + + """ + + def current_namespace(): + current = namespace + # When inside a namespace Maya adds no trailing : + if not current.endswith(":"): + current += ":" + return current + + # Always check against the absolute namespace root + # There's no clash with :x if we're defining namespace :a:x + ROOT = ":" if namespace.startswith(":") else current_namespace() + + # Strip trailing `:` tokens since we might want to add a suffix + start = ":" if namespace.startswith(":") else "" + end = ":" if namespace.endswith(":") else "" + namespace = namespace.strip(":") + if ":" in namespace: + # Split off any nesting that we don't uniqify anyway. + parents, namespace = namespace.rsplit(":", 1) + start += parents + ":" + ROOT += start + + iteration = 1 + increment_version = True + while increment_version: + nr_namespace = namespace + format % iteration + unique = prefix + nr_namespace + suffix + container_name = f"{unique}:{namespace}{con_suffix}" + if not rt.getNodeByName(container_name): + name_space = start + unique + end + increment_version = False + return name_space + else: + increment_version = True + iteration +=1 diff --git a/openpype/hosts/max/api/pipeline.py b/openpype/hosts/max/api/pipeline.py index f58bd05a13..459c8b32f0 100644 --- a/openpype/hosts/max/api/pipeline.py +++ b/openpype/hosts/max/api/pipeline.py @@ -154,17 +154,18 @@ def ls() -> list: yield lib.read(container) -def containerise(name: str, nodes: list, context, loader=None, suffix="_CON"): +def containerise(name: str, nodes: list, context, + namespace=None, loader=None, suffix="_CON"): data = { "schema": "openpype:container-2.0", "id": AVALON_CONTAINER_ID, "name": name, - "namespace": "", + "namespace": namespace, "loader": loader, "representation": context["representation"]["_id"], } - container_name = f"{name}{suffix}" + container_name = f"{namespace}:{name}{suffix}" container = rt.container(name=container_name) for node in nodes: node.Parent = container diff --git a/openpype/hosts/max/plugins/load/load_camera_fbx.py b/openpype/hosts/max/plugins/load/load_camera_fbx.py index 86e201afa8..180c1b48b8 100644 --- a/openpype/hosts/max/plugins/load/load_camera_fbx.py +++ b/openpype/hosts/max/plugins/load/load_camera_fbx.py @@ -1,6 +1,7 @@ import os from openpype.hosts.max.api import lib, maintained_selection +from openpype.hosts.max.api.lib import unique_namespace from openpype.hosts.max.api.pipeline import ( containerise, import_custom_attribute_data, @@ -38,8 +39,14 @@ class FbxLoader(load.LoaderPlugin): for selection in selections: selection.Parent = container + namespace = unique_namespace( + name + "_", + suffix="_", + ) + return containerise( - name, [container], context, loader=self.__class__.__name__) + name, [container], context, + namespace, loader=self.__class__.__name__) def update(self, container, representation): from pymxs import runtime as rt diff --git a/openpype/hosts/max/plugins/load/load_max_scene.py b/openpype/hosts/max/plugins/load/load_max_scene.py index 9c7468b8fc..7c00706676 100644 --- a/openpype/hosts/max/plugins/load/load_max_scene.py +++ b/openpype/hosts/max/plugins/load/load_max_scene.py @@ -1,6 +1,7 @@ import os from openpype.hosts.max.api import lib +from openpype.hosts.max.api.lib import unique_namespace from openpype.hosts.max.api.pipeline import ( containerise, import_custom_attribute_data, update_custom_attribute_data @@ -34,8 +35,15 @@ class MaxSceneLoader(load.LoaderPlugin): import_custom_attribute_data(container, max_objects) max_container.append(container) max_container.extend(max_objects) + + namespace = unique_namespace( + name + "_", + suffix="_", + ) + return containerise( - name, max_container, context, loader=self.__class__.__name__) + name, max_container, context, + namespace, loader=self.__class__.__name__) def update(self, container, representation): from pymxs import runtime as rt diff --git a/openpype/hosts/max/plugins/load/load_model.py b/openpype/hosts/max/plugins/load/load_model.py index a84d497aab..ebf3d684c8 100644 --- a/openpype/hosts/max/plugins/load/load_model.py +++ b/openpype/hosts/max/plugins/load/load_model.py @@ -6,7 +6,9 @@ from openpype.hosts.max.api.pipeline import ( update_custom_attribute_data ) from openpype.hosts.max.api import lib -from openpype.hosts.max.api.lib import maintained_selection +from openpype.hosts.max.api.lib import ( + maintained_selection, unique_namespace +) class ModelAbcLoader(load.LoaderPlugin): @@ -51,8 +53,15 @@ class ModelAbcLoader(load.LoaderPlugin): abc_container = abc_containers.pop() import_custom_attribute_data( abc_container, abc_container.Children) + + namespace = unique_namespace( + name + "_", + suffix="_", + ) + return containerise( - name, [abc_container], context, loader=self.__class__.__name__ + name, [abc_container], context, + namespace, loader=self.__class__.__name__ ) def update(self, container, representation): diff --git a/openpype/hosts/max/plugins/load/load_model_fbx.py b/openpype/hosts/max/plugins/load/load_model_fbx.py index 67252a73ff..34ac263821 100644 --- a/openpype/hosts/max/plugins/load/load_model_fbx.py +++ b/openpype/hosts/max/plugins/load/load_model_fbx.py @@ -4,6 +4,7 @@ from openpype.hosts.max.api.pipeline import ( containerise, import_custom_attribute_data, update_custom_attribute_data ) from openpype.hosts.max.api import lib +from openpype.hosts.max.api.lib import unique_namespace from openpype.hosts.max.api.lib import maintained_selection @@ -36,8 +37,13 @@ class FbxModelLoader(load.LoaderPlugin): for selection in selections: selection.Parent = container + namespace = unique_namespace( + name + "_", + suffix="_", + ) return containerise( - name, [container], context, loader=self.__class__.__name__ + name, [container], context, + namespace, loader=self.__class__.__name__ ) def update(self, container, representation): diff --git a/openpype/hosts/max/plugins/load/load_model_obj.py b/openpype/hosts/max/plugins/load/load_model_obj.py index 9979ca36b0..e4ae687802 100644 --- a/openpype/hosts/max/plugins/load/load_model_obj.py +++ b/openpype/hosts/max/plugins/load/load_model_obj.py @@ -1,6 +1,7 @@ import os from openpype.hosts.max.api import lib +from openpype.hosts.max.api.lib import unique_namespace from openpype.hosts.max.api.lib import maintained_selection from openpype.hosts.max.api.pipeline import ( containerise, @@ -34,8 +35,13 @@ class ObjLoader(load.LoaderPlugin): for selection in selections: selection.Parent = container + namespace = unique_namespace( + name + "_", + suffix="_", + ) return containerise( - name, [container], context, loader=self.__class__.__name__) + name, [container], context, + namespace, loader=self.__class__.__name__) def update(self, container, representation): from pymxs import runtime as rt diff --git a/openpype/hosts/max/plugins/load/load_model_usd.py b/openpype/hosts/max/plugins/load/load_model_usd.py index d3669fc10e..fa013f54ce 100644 --- a/openpype/hosts/max/plugins/load/load_model_usd.py +++ b/openpype/hosts/max/plugins/load/load_model_usd.py @@ -1,6 +1,7 @@ import os from openpype.hosts.max.api import lib +from openpype.hosts.max.api.lib import unique_namespace from openpype.hosts.max.api.lib import maintained_selection from openpype.hosts.max.api.pipeline import ( containerise, @@ -38,8 +39,14 @@ class ModelUSDLoader(load.LoaderPlugin): import_custom_attribute_data(asset, asset.Children) + namespace = unique_namespace( + name + "_", + suffix="_", + ) + return containerise( - name, [asset], context, loader=self.__class__.__name__) + name, [asset], context, + namespace, loader=self.__class__.__name__) def update(self, container, representation): from pymxs import runtime as rt diff --git a/openpype/hosts/max/plugins/load/load_pointcache.py b/openpype/hosts/max/plugins/load/load_pointcache.py index 953141c4ac..3dacab11c7 100644 --- a/openpype/hosts/max/plugins/load/load_pointcache.py +++ b/openpype/hosts/max/plugins/load/load_pointcache.py @@ -7,6 +7,7 @@ Because of limited api, alembics can be only loaded, but not easily updated. import os from openpype.pipeline import load, get_representation_path from openpype.hosts.max.api import lib, maintained_selection +from openpype.hosts.max.api.lib import unique_namespace from openpype.hosts.max.api.pipeline import ( containerise, import_custom_attribute_data, @@ -59,9 +60,14 @@ class AbcLoader(load.LoaderPlugin): for cam_shape in abc.Children: cam_shape.playbackType = 2 + namespace = unique_namespace( + name + "_", + suffix="_", + ) return containerise( - name, [abc_container], context, loader=self.__class__.__name__ + name, [abc_container], context, + namespace, loader=self.__class__.__name__ ) def update(self, container, representation): diff --git a/openpype/hosts/max/plugins/load/load_pointcloud.py b/openpype/hosts/max/plugins/load/load_pointcloud.py index c263019beb..58d5057aa7 100644 --- a/openpype/hosts/max/plugins/load/load_pointcloud.py +++ b/openpype/hosts/max/plugins/load/load_pointcloud.py @@ -1,6 +1,7 @@ import os from openpype.hosts.max.api import lib, maintained_selection +from openpype.hosts.max.api.lib import unique_namespace from openpype.hosts.max.api.pipeline import ( containerise, import_custom_attribute_data, @@ -31,8 +32,14 @@ class PointCloudLoader(load.LoaderPlugin): obj.Parent = prt_container import_custom_attribute_data(prt_container, [obj]) + namespace = unique_namespace( + name + "_", + suffix="_", + ) + return containerise( - name, [prt_container], context, loader=self.__class__.__name__) + name, [prt_container], context, + namespace, loader=self.__class__.__name__) def update(self, container, representation): """update the container""" diff --git a/openpype/hosts/max/plugins/load/load_redshift_proxy.py b/openpype/hosts/max/plugins/load/load_redshift_proxy.py index 6b100df611..b4772ac0bc 100644 --- a/openpype/hosts/max/plugins/load/load_redshift_proxy.py +++ b/openpype/hosts/max/plugins/load/load_redshift_proxy.py @@ -11,6 +11,7 @@ from openpype.hosts.max.api.pipeline import ( update_custom_attribute_data ) from openpype.hosts.max.api import lib +from openpype.hosts.max.api.lib import unique_namespace class RedshiftProxyLoader(load.LoaderPlugin): @@ -40,8 +41,14 @@ class RedshiftProxyLoader(load.LoaderPlugin): import_custom_attribute_data(container, [rs_proxy]) asset = rt.getNodeByName(name) + namespace = unique_namespace( + name + "_", + suffix="_", + ) + return containerise( - name, [asset], context, loader=self.__class__.__name__) + name, [asset], context, + namespace, loader=self.__class__.__name__) def update(self, container, representation): from pymxs import runtime as rt From 5a00cab24cd1f0dc4dd2988f28fa9b7b88b0b63b Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 17 Aug 2023 21:24:10 +0800 Subject: [PATCH 129/327] hound --- openpype/hosts/max/api/lib.py | 2 +- openpype/hosts/max/api/pipeline.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index b58b4f5b11..e357080cbc 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -367,4 +367,4 @@ def unique_namespace(namespace, format="%02d", return name_space else: increment_version = True - iteration +=1 + iteration += 1 diff --git a/openpype/hosts/max/api/pipeline.py b/openpype/hosts/max/api/pipeline.py index 459c8b32f0..161e2bdc7b 100644 --- a/openpype/hosts/max/api/pipeline.py +++ b/openpype/hosts/max/api/pipeline.py @@ -160,7 +160,7 @@ def containerise(name: str, nodes: list, context, "schema": "openpype:container-2.0", "id": AVALON_CONTAINER_ID, "name": name, - "namespace": namespace, + "namespace": namespace or "", "loader": loader, "representation": context["representation"]["_id"], } From ab3d94fdb6f607b958704a06d3fa87187ac03d71 Mon Sep 17 00:00:00 2001 From: Fabia Serra Arrizabalaga Date: Thu, 17 Aug 2023 15:59:46 -0700 Subject: [PATCH 130/327] Move variant query to the create_interactive function --- .../hosts/houdini/api/creator_node_shelves.py | 47 ++++++++++--------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/openpype/hosts/houdini/api/creator_node_shelves.py b/openpype/hosts/houdini/api/creator_node_shelves.py index 01da2fc3e1..1f9fef7417 100644 --- a/openpype/hosts/houdini/api/creator_node_shelves.py +++ b/openpype/hosts/houdini/api/creator_node_shelves.py @@ -35,11 +35,11 @@ CATEGORY_GENERIC_TOOL = { CREATE_SCRIPT = """ from openpype.hosts.houdini.api.creator_node_shelves import create_interactive -create_interactive("{identifier}", "{variant}", **kwargs) +create_interactive("{identifier}", **kwargs) """ -def create_interactive(creator_identifier, default_variant, **kwargs): +def create_interactive(creator_identifier, **kwargs): """Create a Creator using its identifier interactively. This is used by the generated shelf tools as callback when a user selects @@ -57,28 +57,31 @@ def create_interactive(creator_identifier, default_variant, **kwargs): list: The created instances. """ - - # TODO Use Qt instead - result, variant = hou.ui.readInput("Define variant name", - buttons=("Ok", "Cancel"), - initial_contents=default_variant, - title="Define variant", - help="Set the variant for the " - "publish instance", - close_choice=1) - if result == 1: - # User interrupted - return - variant = variant.strip() - if not variant: - raise RuntimeError("Empty variant value entered.") - host = registered_host() context = CreateContext(host) creator = context.manual_creators.get(creator_identifier) if not creator: - raise RuntimeError("Invalid creator identifier: " - "{}".format(creator_identifier)) + raise RuntimeError("Invalid creator identifier: {}".format( + creator_identifier) + ) + + # TODO Use Qt instead + result, variant = hou.ui.readInput( + "Define variant name", + buttons=("Ok", "Cancel"), + initial_contents=creator.get_default_variant(), + title="Define variant", + help="Set the variant for the publish instance", + close_choice=1 + ) + + if result == 1: + # User interrupted + return + + variant = variant.strip() + if not variant: + raise RuntimeError("Empty variant value entered.") # TODO: Once more elaborate unique create behavior should exist per Creator # instead of per network editor area then we should move this from here @@ -196,9 +199,7 @@ def install(): key = "openpype_create.{}".format(identifier) log.debug(f"Registering {key}") - script = CREATE_SCRIPT.format( - identifier=identifier, variant=creator.get_default_variant() - ) + script = CREATE_SCRIPT.format(identifier=identifier) data = { "script": script, "language": hou.scriptLanguage.Python, From f4e42e27ac01faa61c71b2dec930a78361645578 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 18 Aug 2023 18:43:13 +0800 Subject: [PATCH 131/327] updating version should be updated as expected --- .../hosts/max/plugins/load/load_camera_fbx.py | 15 ++++++++--- .../hosts/max/plugins/load/load_max_scene.py | 15 +++++++---- openpype/hosts/max/plugins/load/load_model.py | 26 ++++++++----------- .../hosts/max/plugins/load/load_model_fbx.py | 17 +++++++----- .../hosts/max/plugins/load/load_model_obj.py | 8 +++--- .../hosts/max/plugins/load/load_model_usd.py | 11 +++++--- .../hosts/max/plugins/load/load_pointcache.py | 14 ++++------ .../hosts/max/plugins/load/load_pointcloud.py | 14 +++++----- .../max/plugins/load/load_redshift_proxy.py | 15 +++++------ 9 files changed, 75 insertions(+), 60 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_camera_fbx.py b/openpype/hosts/max/plugins/load/load_camera_fbx.py index 180c1b48b8..c0e1172a6d 100644 --- a/openpype/hosts/max/plugins/load/load_camera_fbx.py +++ b/openpype/hosts/max/plugins/load/load_camera_fbx.py @@ -54,7 +54,10 @@ class FbxLoader(load.LoaderPlugin): path = get_representation_path(representation) node_name = container["instance_node"] node = rt.getNodeByName(node_name) - inst_name, _ = node_name.split("_") + container_name = node_name.split(":")[-1] + param_container, _ = container_name.split("_") + + inst_container = rt.getNodeByName(param_container) rt.Select(node.Children) rt.FBXImporterSetParam("Animation", True) @@ -65,12 +68,16 @@ class FbxLoader(load.LoaderPlugin): rt.ImportFile( path, rt.name("noPrompt"), using=rt.FBXIMP) current_fbx_objects = rt.GetCurrentSelection() - inst_container = rt.getNodeByName(inst_name) for fbx_object in current_fbx_objects: if fbx_object.Parent != inst_container: fbx_object.Parent = inst_container - update_custom_attribute_data( - inst_container, rt.GetCurrentSelection()) + + for children in node.Children: + if rt.classOf(children) == rt.Container: + if children.name == param_container: + update_custom_attribute_data( + children, current_fbx_objects) + with maintained_selection(): rt.Select(node) diff --git a/openpype/hosts/max/plugins/load/load_max_scene.py b/openpype/hosts/max/plugins/load/load_max_scene.py index 7c00706676..aa177291d8 100644 --- a/openpype/hosts/max/plugins/load/load_max_scene.py +++ b/openpype/hosts/max/plugins/load/load_max_scene.py @@ -40,7 +40,6 @@ class MaxSceneLoader(load.LoaderPlugin): name + "_", suffix="_", ) - return containerise( name, max_container, context, namespace, loader=self.__class__.__name__) @@ -50,9 +49,11 @@ class MaxSceneLoader(load.LoaderPlugin): path = get_representation_path(representation) node_name = container["instance_node"] + node = rt.getNodeByName(node_name) - inst_name, _ = node_name.split("_") - inst_container = rt.getNodeByName(inst_name) + container_name = node_name.split(":")[-1] + param_container, _ = container_name.split("_") + # delete the old container with attribute # delete old duplicate prev_max_object_names = [obj.name for obj @@ -71,10 +72,14 @@ class MaxSceneLoader(load.LoaderPlugin): prev_max_object = rt.getNodeByName(object_name) rt.Delete(prev_max_object) - update_custom_attribute_data(inst_container, current_max_objects) - for max_object in current_max_objects: max_object.Parent = node + for children in node.Children: + if rt.classOf(children) == rt.Container: + if children.name == param_container: + update_custom_attribute_data( + children, current_max_objects) + lib.imprint(container["instance_node"], { "representation": str(representation["_id"]) }) diff --git a/openpype/hosts/max/plugins/load/load_model.py b/openpype/hosts/max/plugins/load/load_model.py index ebf3d684c8..deb3389992 100644 --- a/openpype/hosts/max/plugins/load/load_model.py +++ b/openpype/hosts/max/plugins/load/load_model.py @@ -69,23 +69,19 @@ class ModelAbcLoader(load.LoaderPlugin): path = get_representation_path(representation) node = rt.GetNodeByName(container["instance_node"]) - rt.Select(node.Children) - nodes_list = [] with maintained_selection(): - rt.Select(node) - for alembic in rt.Selection: - abc = rt.GetNodeByName(alembic.name) - update_custom_attribute_data(abc, abc.Children) - rt.Select(abc.Children) - for abc_con in rt.Selection: - abc_container = rt.GetNodeByName(abc_con.name) - abc_container.source = path - rt.Select(abc_container.Children) - for abc_obj in rt.Selection: - alembic_obj = rt.GetNodeByName(abc_obj.name) - alembic_obj.source = path - nodes_list.append(alembic_obj) + rt.Select(node.Children) + + for alembic in rt.Selection: + abc = rt.GetNodeByName(alembic.name) + update_custom_attribute_data(abc, abc.Children) + rt.Select(abc.Children) + for abc_con in abc.Children: + abc_con.source = path + rt.Select(abc_con.Children) + for abc_obj in abc_con.Children: + abc_obj.source = path lib.imprint( container["instance_node"], diff --git a/openpype/hosts/max/plugins/load/load_model_fbx.py b/openpype/hosts/max/plugins/load/load_model_fbx.py index 34ac263821..f85bfa03a1 100644 --- a/openpype/hosts/max/plugins/load/load_model_fbx.py +++ b/openpype/hosts/max/plugins/load/load_model_fbx.py @@ -1,7 +1,8 @@ import os from openpype.pipeline import load, get_representation_path from openpype.hosts.max.api.pipeline import ( - containerise, import_custom_attribute_data, update_custom_attribute_data + containerise, import_custom_attribute_data, + update_custom_attribute_data ) from openpype.hosts.max.api import lib from openpype.hosts.max.api.lib import unique_namespace @@ -51,9 +52,8 @@ class FbxModelLoader(load.LoaderPlugin): path = get_representation_path(representation) node_name = container["instance_node"] node = rt.getNodeByName(node_name) - inst_name, _ = node_name.split("_") - inst_container = rt.getNodeByName(inst_name) - + container_name = node_name.split(":")[-1] + param_container, _ = container_name.split("_") rt.FBXImporterSetParam("Animation", False) rt.FBXImporterSetParam("Cameras", False) rt.FBXImporterSetParam("Mode", rt.Name("merge")) @@ -61,11 +61,16 @@ class FbxModelLoader(load.LoaderPlugin): rt.FBXImporterSetParam("Preserveinstances", True) rt.importFile(path, rt.name("noPrompt"), using=rt.FBXIMP) current_fbx_objects = rt.GetCurrentSelection() + + inst_container = rt.getNodeByName(param_container) + for children in node.Children: + if rt.classOf(children) == rt.Container: + if children.name == param_container: + update_custom_attribute_data( + children, current_fbx_objects) for fbx_object in current_fbx_objects: if fbx_object.Parent != inst_container: fbx_object.Parent = inst_container - update_custom_attribute_data( - inst_container, current_fbx_objects) with maintained_selection(): rt.Select(node) diff --git a/openpype/hosts/max/plugins/load/load_model_obj.py b/openpype/hosts/max/plugins/load/load_model_obj.py index e4ae687802..b42ef399b0 100644 --- a/openpype/hosts/max/plugins/load/load_model_obj.py +++ b/openpype/hosts/max/plugins/load/load_model_obj.py @@ -50,9 +50,11 @@ class ObjLoader(load.LoaderPlugin): node_name = container["instance_node"] node = rt.GetNodeByName(node_name) - instance_name, _ = node_name.split("_") - inst_container = rt.GetNodeByName(instance_name) - for child in container.Children: + container_name = node_name.split(":")[-1] + param_container, _ = container_name.split("_") + + inst_container = rt.getNodeByName(param_container) + for child in inst_container.Children: rt.Delete(child) rt.Execute(f'importFile @"{path}" #noPrompt using:ObjImp') diff --git a/openpype/hosts/max/plugins/load/load_model_usd.py b/openpype/hosts/max/plugins/load/load_model_usd.py index fa013f54ce..4febba216e 100644 --- a/openpype/hosts/max/plugins/load/load_model_usd.py +++ b/openpype/hosts/max/plugins/load/load_model_usd.py @@ -58,7 +58,8 @@ class ModelUSDLoader(load.LoaderPlugin): for r in n.Children: rt.Delete(r) rt.Delete(n) - instance_name, _ = node_name.split("_") + container_name = node_name.split(":")[-1] + param_container, _ = container_name.split("_") import_options = rt.USDImporter.CreateOptions() base_filename = os.path.basename(path) @@ -70,9 +71,13 @@ class ModelUSDLoader(load.LoaderPlugin): rt.USDImporter.importFile( path, importOptions=import_options) - asset = rt.GetNodeByName(instance_name) + asset = rt.GetNodeByName(param_container) asset.Parent = node - update_custom_attribute_data(asset, asset.Children) + for children in node.Children: + if rt.classOf(children) == rt.Container: + if children.name == param_container: + update_custom_attribute_data( + asset, asset.Children) with maintained_selection(): rt.Select(node) diff --git a/openpype/hosts/max/plugins/load/load_pointcache.py b/openpype/hosts/max/plugins/load/load_pointcache.py index 3dacab11c7..af03e70236 100644 --- a/openpype/hosts/max/plugins/load/load_pointcache.py +++ b/openpype/hosts/max/plugins/load/load_pointcache.py @@ -76,7 +76,6 @@ class AbcLoader(load.LoaderPlugin): path = get_representation_path(representation) node = rt.GetNodeByName(container["instance_node"]) - nodes_list = [] with maintained_selection(): rt.Select(node.Children) @@ -84,14 +83,11 @@ class AbcLoader(load.LoaderPlugin): abc = rt.GetNodeByName(alembic.name) update_custom_attribute_data(abc, abc.Children) rt.Select(abc.Children) - for abc_con in rt.Selection: - abc_container = rt.GetNodeByName(abc_con.name) - abc_container.source = path - rt.Select(abc_container.Children) - for abc_obj in rt.Selection: - alembic_obj = rt.GetNodeByName(abc_obj.name) - alembic_obj.source = path - nodes_list.append(alembic_obj) + for abc_con in abc.Children: + abc_con.source = path + rt.Select(abc_con.Children) + for abc_obj in abc_con.Children: + abc_obj.source = path lib.imprint( container["instance_node"], diff --git a/openpype/hosts/max/plugins/load/load_pointcloud.py b/openpype/hosts/max/plugins/load/load_pointcloud.py index 58d5057aa7..6c94fb7847 100644 --- a/openpype/hosts/max/plugins/load/load_pointcloud.py +++ b/openpype/hosts/max/plugins/load/load_pointcloud.py @@ -26,9 +26,7 @@ class PointCloudLoader(load.LoaderPlugin): filepath = os.path.normpath(self.filepath_from_context(context)) obj = rt.tyCache() obj.filename = filepath - prt_container = rt.GetNodeByName(obj.name) - prt_container = rt.container() - prt_container.name = name + prt_container = rt.Container(name=name) obj.Parent = prt_container import_custom_attribute_data(prt_container, [obj]) @@ -49,10 +47,12 @@ class PointCloudLoader(load.LoaderPlugin): node = rt.GetNodeByName(container["instance_node"]) with maintained_selection(): rt.Select(node.Children) - for prt in rt.Selection: - prt_object = rt.GetNodeByName(prt.name) - prt_object.filename = path - update_custom_attribute_data(node, node.Children) + for sub_node in rt.Selection: + children_node = sub_node.Children + update_custom_attribute_data( + sub_node, sub_node.Children) + for prt in children_node: + prt.filename = path lib.imprint(container["instance_node"], { "representation": str(representation["_id"]) }) diff --git a/openpype/hosts/max/plugins/load/load_redshift_proxy.py b/openpype/hosts/max/plugins/load/load_redshift_proxy.py index b4772ac0bc..1c4cd02143 100644 --- a/openpype/hosts/max/plugins/load/load_redshift_proxy.py +++ b/openpype/hosts/max/plugins/load/load_redshift_proxy.py @@ -35,11 +35,9 @@ class RedshiftProxyLoader(load.LoaderPlugin): if collections: rs_proxy.is_sequence = True - container = rt.container() - container.name = name + container = rt.Container(name=name) rs_proxy.Parent = container import_custom_attribute_data(container, [rs_proxy]) - asset = rt.getNodeByName(name) namespace = unique_namespace( name + "_", @@ -47,7 +45,7 @@ class RedshiftProxyLoader(load.LoaderPlugin): ) return containerise( - name, [asset], context, + name, [container], context, namespace, loader=self.__class__.__name__) def update(self, container, representation): @@ -55,12 +53,13 @@ class RedshiftProxyLoader(load.LoaderPlugin): path = get_representation_path(representation) node = rt.getNodeByName(container["instance_node"]) - for children in node.Children: - children_node = rt.getNodeByName(children.name) - for proxy in children_node.Children: + for sub_node in node.Children: + children_node = sub_node.Children + update_custom_attribute_data( + sub_node, children_node) + for proxy in children_node: proxy.file = path - update_custom_attribute_data(node, node.Children) lib.imprint(container["instance_node"], { "representation": str(representation["_id"]) }) From 3ba4e7cbffed7ff7ae396872ba500bcc92292348 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 19 Aug 2023 03:24:22 +0000 Subject: [PATCH 132/327] [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 70eb32baff..444721e19c 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.16.4-nightly.2" +__version__ = "3.16.4-nightly.3" From a25d1742a7a981f39f5580e1e27e0b1aa2eb9a33 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 19 Aug 2023 03:25:04 +0000 Subject: [PATCH 133/327] 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 d2a4067a6a..326c9e8c86 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.16.4-nightly.3 - 3.16.4-nightly.2 - 3.16.4-nightly.1 - 3.16.3 @@ -134,7 +135,6 @@ body: - 3.14.8-nightly.1 - 3.14.7 - 3.14.7-nightly.8 - - 3.14.7-nightly.7 validations: required: true - type: dropdown From a63fef653d536167aace6267d2b6246ee3581205 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 21 Aug 2023 10:32:32 +0200 Subject: [PATCH 134/327] Context plugin shouldn't be tied to family (#5464) --- openpype/hosts/maya/plugins/publish/collect_current_file.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_current_file.py b/openpype/hosts/maya/plugins/publish/collect_current_file.py index e777a209d4..c7105a7f3c 100644 --- a/openpype/hosts/maya/plugins/publish/collect_current_file.py +++ b/openpype/hosts/maya/plugins/publish/collect_current_file.py @@ -10,7 +10,6 @@ class CollectCurrentFile(pyblish.api.ContextPlugin): order = pyblish.api.CollectorOrder - 0.4 label = "Maya Current File" hosts = ['maya'] - families = ["workfile"] def process(self, context): """Inject the current working file""" From e742dd61fd85c7cac3d5027ae758bef2f7aa5af7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 21 Aug 2023 14:42:02 +0200 Subject: [PATCH 135/327] fill entities and update template data on instances during extract AYON hierarchy --- .../publish/extract_hierarchy_to_ayon.py | 44 ++++++++++++++++++- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/extract_hierarchy_to_ayon.py b/openpype/plugins/publish/extract_hierarchy_to_ayon.py index 915650ae41..de9a70c233 100644 --- a/openpype/plugins/publish/extract_hierarchy_to_ayon.py +++ b/openpype/plugins/publish/extract_hierarchy_to_ayon.py @@ -8,6 +8,11 @@ from ayon_api import slugify_string from ayon_api.entity_hub import EntityHub from openpype import AYON_SERVER_ENABLED +from openpype.client import get_assets +from openpype.pipeline.template_data import ( + get_asset_template_data, + get_task_template_data, +) def _default_json_parse(value): @@ -27,13 +32,48 @@ class ExtractHierarchyToAYON(pyblish.api.ContextPlugin): hierarchy_context = context.data.get("hierarchyContext") if not hierarchy_context: - self.log.info("Skipping") + self.log.debug("Skipping") return project_name = context.data["projectName"] + self._create_hierarchy(context, project_name) + self._fill_instance_entities(context, project_name) + + def _fill_instance_entities(self, context, project_name): + instances_by_asset_name = collections.defaultdict(list) + for instance in context: + instance_entity = instance.data.get("assetEntity") + if instance_entity: + continue + + # Skip if instance asset does not match + instance_asset_name = instance.data.get("asset") + instances_by_asset_name[instance_asset_name] = instance + + project_doc = context.data["projectEntity"] + asset_docs = get_assets( + project_name, asset_names=instances_by_asset_name.keys() + ) + asset_docs_by_name = { + asset_doc["name"]: asset_doc + for asset_doc in asset_docs + } + for asset_name, instances in instances_by_asset_name.items(): + asset_doc = asset_docs_by_name[asset_name] + asset_data = get_asset_template_data(asset_doc, project_name) + for instance in instances: + task_name = instance.data.get("task") + template_data = get_task_template_data( + project_doc, asset_doc, task_name) + template_data.update(copy.deepcopy(asset_data)) + + instance.data["anatomyData"].update(template_data) + instance.data["assetEntity"] = asset_doc + + def _create_hierarchy(self, context, project_name): hierarchy_context = self._filter_hierarchy(context) if not hierarchy_context: - self.log.info("All folders were filtered out") + self.log.debug("All folders were filtered out") return self.log.debug("Hierarchy_context: {}".format( From 20c1c1ce829b8d217ff0e91a452eba73e7861488 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 21 Aug 2023 18:17:38 +0200 Subject: [PATCH 136/327] AYON: Fix version attributes update (#5472) * fix attrib update * proper fix of attrib updates --- openpype/client/server/conversion_utils.py | 26 +++++++++++++--------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/openpype/client/server/conversion_utils.py b/openpype/client/server/conversion_utils.py index 42df337b6d..a6c190a0fc 100644 --- a/openpype/client/server/conversion_utils.py +++ b/openpype/client/server/conversion_utils.py @@ -1074,7 +1074,7 @@ def convert_update_folder_to_v4(project_name, asset_id, update_data, con): parent_id = None tasks = None new_data = {} - attribs = {} + attribs = full_update_data.pop("attrib", {}) if "type" in update_data: new_update_data["active"] = update_data["type"] == "asset" @@ -1113,6 +1113,9 @@ def convert_update_folder_to_v4(project_name, asset_id, update_data, con): print("Folder has new data: {}".format(new_data)) new_update_data["data"] = new_data + if attribs: + new_update_data["attrib"] = attribs + if has_task_changes: raise ValueError("Task changes of folder are not implemented") @@ -1126,7 +1129,7 @@ def convert_update_subset_to_v4(project_name, subset_id, update_data, con): full_update_data = _from_flat_dict(update_data) data = full_update_data.get("data") new_data = {} - attribs = {} + attribs = full_update_data.pop("attrib", {}) if data: if "family" in data: family = data.pop("family") @@ -1148,9 +1151,6 @@ def convert_update_subset_to_v4(project_name, subset_id, update_data, con): elif value is not REMOVED_VALUE: new_data[key] = value - if attribs: - new_update_data["attribs"] = attribs - if "name" in update_data: new_update_data["name"] = update_data["name"] @@ -1165,6 +1165,9 @@ def convert_update_subset_to_v4(project_name, subset_id, update_data, con): new_update_data["folderId"] = update_data["parent"] flat_data = _to_flat_dict(new_update_data) + if attribs: + flat_data["attrib"] = attribs + if new_data: print("Subset has new data: {}".format(new_data)) flat_data["data"] = new_data @@ -1179,7 +1182,7 @@ def convert_update_version_to_v4(project_name, version_id, update_data, con): full_update_data = _from_flat_dict(update_data) data = full_update_data.get("data") new_data = {} - attribs = {} + attribs = full_update_data.pop("attrib", {}) if data: if "author" in data: new_update_data["author"] = data.pop("author") @@ -1196,9 +1199,6 @@ def convert_update_version_to_v4(project_name, version_id, update_data, con): elif value is not REMOVED_VALUE: new_data[key] = value - if attribs: - new_update_data["attribs"] = attribs - if "name" in update_data: new_update_data["version"] = update_data["name"] @@ -1213,6 +1213,9 @@ def convert_update_version_to_v4(project_name, version_id, update_data, con): new_update_data["productId"] = update_data["parent"] flat_data = _to_flat_dict(new_update_data) + if attribs: + flat_data["attrib"] = attribs + if new_data: print("Version has new data: {}".format(new_data)) flat_data["data"] = new_data @@ -1252,7 +1255,7 @@ def convert_update_representation_to_v4( data = full_update_data.get("data") new_data = {} - attribs = {} + attribs = full_update_data.pop("attrib", {}) if data: for key, value in data.items(): if key in folder_attributes: @@ -1309,6 +1312,9 @@ def convert_update_representation_to_v4( new_update_data["files"] = new_files flat_data = _to_flat_dict(new_update_data) + if attribs: + flat_data["attrib"] = attribs + if new_data: print("Representation has new data: {}".format(new_data)) flat_data["data"] = new_data From 32b93f998aa95c3647797040b4a73fd1e68a305f Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 22 Aug 2023 12:47:36 +0800 Subject: [PATCH 137/327] allows the users to choose which camera as repair action in validate_viewport_camera --- openpype/hosts/max/api/lib_rendersettings.py | 5 +++-- .../publish/validate_viewport_camera.py | 19 +++++++++++-------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/max/api/lib_rendersettings.py b/openpype/hosts/max/api/lib_rendersettings.py index 1b62edabee..afde5008d5 100644 --- a/openpype/hosts/max/api/lib_rendersettings.py +++ b/openpype/hosts/max/api/lib_rendersettings.py @@ -43,7 +43,7 @@ class RenderSettings(object): rt.viewport.setCamera(sel) break if not found: - raise RuntimeError("Camera not found") + raise RuntimeError("Active Camera not found") def render_output(self, container): folder = rt.maxFilePath @@ -113,7 +113,8 @@ class RenderSettings(object): # for setting up renderable camera arv = rt.MAXToAOps.ArnoldRenderView() render_camera = rt.viewport.GetCamera() - arv.setOption("Camera", str(render_camera)) + if render_camera: + arv.setOption("Camera", str(render_camera)) # TODO: add AOVs and extension img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] # noqa diff --git a/openpype/hosts/max/plugins/publish/validate_viewport_camera.py b/openpype/hosts/max/plugins/publish/validate_viewport_camera.py index d5cf85eb69..b35ba482a9 100644 --- a/openpype/hosts/max/plugins/publish/validate_viewport_camera.py +++ b/openpype/hosts/max/plugins/publish/validate_viewport_camera.py @@ -4,6 +4,7 @@ from openpype.pipeline import ( PublishValidationError, OptionalPyblishPluginMixin) from openpype.pipeline.publish import RepairAction +from openpype.hosts.max.api.lib import get_current_renderer from pymxs import runtime as rt @@ -35,11 +36,13 @@ class ValidateViewportCamera(pyblish.api.InstancePlugin, @classmethod def repair(cls, instance): - # Get all cameras in the scene - cameras_in_scene = [c.name for c in rt.Objects - if rt.classOf(c) in rt.Camera.Classes] - # Set the first camera as viewport camera for rendering - if cameras_in_scene: - camera = rt.getNodeByName(cameras_in_scene[0]) - rt.viewport.setCamera(camera) - cls.log.info(f"Camera {camera} set as viewport camera") + + rt.viewport.setType(rt.Name("view_camera")) + camera = rt.viewport.GetCamera() + cls.log.info(f"Camera {camera} set as viewport camera") + renderer_class = get_current_renderer() + renderer = str(renderer_class).split(":")[0] + if renderer == "Arnold": + arv = rt.MAXToAOps.ArnoldRenderView() + arv.setOption("Camera", str(camera)) + arv.close() From 6376692fec24267af13f7900e2ac60fd65aed373 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 22 Aug 2023 13:56:47 +0200 Subject: [PATCH 138/327] :recycle: use temp dir for project creation --- .../unreal/hooks/pre_workfile_preparation.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py index 202d7854f6..fad7a7ed0b 100644 --- a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py +++ b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py @@ -2,6 +2,7 @@ """Hook to launch Unreal and prepare projects.""" import os import copy +import tempfile from pathlib import Path from qtpy import QtCore @@ -224,10 +225,19 @@ class UnrealPrelaunchHook(PreLaunchHook): project_file = project_path / unreal_project_filename if not project_file.is_file(): - self.exec_ue_project_gen(engine_version, - unreal_project_name, - engine_path, - project_path) + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) / unreal_project_filename + self.exec_ue_project_gen(engine_version, + unreal_project_name, + engine_path, + temp_path) + try: + temp_path.rename(project_path) + except FileExistsError as e: + raise ApplicationLaunchFailed(( + f"{self.signature} Project folder " + f"already exists {project_path.as_posix()}" + )) from e self.launch_context.env["AYON_UNREAL_VERSION"] = engine_version # Append project file to launch arguments From ec054ce939383f768fbcc3a7c622088e1fd4c1cb Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 22 Aug 2023 13:00:36 +0100 Subject: [PATCH 139/327] Fix version 0 when integrating to Ftrack. --- openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py index 6ca5d1d4ef..4d474fab10 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py @@ -353,7 +353,7 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin): status_name = asset_version_data.pop("status_name", None) # Try query asset version by criteria (asset id and version) - version = asset_version_data.get("version") or 0 + version = asset_version_data.get("version") or "0" asset_version_entity = self._query_asset_version( session, version, asset_id ) From 92165f521ef5f51e46758b3068083339f8d7a14d Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 22 Aug 2023 15:41:24 +0200 Subject: [PATCH 140/327] :bug: fix copy function --- .../unreal/hooks/pre_workfile_preparation.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py index fad7a7ed0b..fb489f04f7 100644 --- a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py +++ b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py @@ -2,6 +2,7 @@ """Hook to launch Unreal and prepare projects.""" import os import copy +import shutil import tempfile from pathlib import Path @@ -230,13 +231,19 @@ class UnrealPrelaunchHook(PreLaunchHook): self.exec_ue_project_gen(engine_version, unreal_project_name, engine_path, - temp_path) + Path(temp_dir)) try: - temp_path.rename(project_path) - except FileExistsError as e: + self.log.info(( + f"Moving from {temp_dir} to " + f"{project_path.as_posix()}" + )) + shutil.copytree( + temp_dir, project_path, dirs_exist_ok=True) + + except shutil.Error as e: raise ApplicationLaunchFailed(( - f"{self.signature} Project folder " - f"already exists {project_path.as_posix()}" + f"{self.signature} Cannot copy directory {temp_dir} " + f"to {project_path.as_posix()} - {e}" )) from e self.launch_context.env["AYON_UNREAL_VERSION"] = engine_version From c2b49a897eee6c33e405a3229bd48bcaee6c8178 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 22 Aug 2023 15:43:48 +0200 Subject: [PATCH 141/327] :recycle: remove unused variable --- openpype/hosts/unreal/hooks/pre_workfile_preparation.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py index fb489f04f7..efbbed27c8 100644 --- a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py +++ b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py @@ -227,7 +227,6 @@ class UnrealPrelaunchHook(PreLaunchHook): if not project_file.is_file(): with tempfile.TemporaryDirectory() as temp_dir: - temp_path = Path(temp_dir) / unreal_project_filename self.exec_ue_project_gen(engine_version, unreal_project_name, engine_path, From 92000bc21e03d3ddf1fc99a62421ead1f278865f Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 22 Aug 2023 15:48:19 +0200 Subject: [PATCH 142/327] :rotating_light: fix hound :dog: --- openpype/hosts/unreal/hooks/pre_workfile_preparation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py index efbbed27c8..a635bd4cab 100644 --- a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py +++ b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py @@ -233,8 +233,8 @@ class UnrealPrelaunchHook(PreLaunchHook): Path(temp_dir)) try: self.log.info(( - f"Moving from {temp_dir} to " - f"{project_path.as_posix()}" + f"Moving from {temp_dir} to " + f"{project_path.as_posix()}" )) shutil.copytree( temp_dir, project_path, dirs_exist_ok=True) From ee31b305d30ff2bb7951af489d6a32ca7ac305e6 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Tue, 22 Aug 2023 14:41:23 +0000 Subject: [PATCH 143/327] [Automated] Release --- CHANGELOG.md | 307 ++++++++++++++++++++++++++++++++++++++++++++ openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 309 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80d6a0d99d..f1948b1a3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,313 @@ # Changelog +## [3.16.4](https://github.com/ynput/OpenPype/tree/3.16.4) + + +[Full Changelog](https://github.com/ynput/OpenPype/compare/3.16.3...3.16.4) + +### **🆕 New features** + + +

+Feature: Download last published workfile specify version #4998 + +Setting `workfile_version` key to hook's `self.launch_context.data` allow you to specify the workfile version you want sync service to download if none is matched locally. This is helpful if the last version hasn't been correctly published/synchronized, and you want to recover the previous one (or some you'd like).Version could be set in two ways: +- OP's absolute version, matching the `version` index in DB. +- Relative version in reverse order from the last one: `-2`, `-3`...I don't know where I should write documentation about that. + + +___ + +
+ +### **🚀 Enhancements** + + +
+Maya: allow not creation of group for Import loaders #5427 + +This PR enhances previous one. All ReferenceLoaders could not wrap imported products into explicit group.Also `Import` Loaders have same options. Control for this is separate in Settings, eg. Reference might wrap loaded items in group, `Import` might not. + + +___ + +
+ + +
+3dsMax: Settings for Ayon #5388 + +Max Addon Setting for Ayon + + +___ + +
+ + +
+General: Navigation to Folder from Launcher #5404 + +Adds an action in launcher to open the directory of the asset. + + +___ + +
+ + +
+Chore: Default variant in create plugin #5429 + +Attribute `default_variant` on create plugins always returns string and if default variant is not filled other ways how to get one are implemented. + + +___ + +
+ + +
+Publisher: Thumbnail widget enhancements #5439 + +Thumbnails widget in Publisher has new 3 options to choose from: Paste (from clipboard), Take screenshot and Browse. Clear button and new options are not visible by default, user must expand options button to show them. + + +___ + +
+ + +
+AYON: Update ayon api to '0.3.5' #5460 + +Updated ayon-python-api to 0.3.5. + + +___ + +
+ +### **🐛 Bug fixes** + + +
+AYON: Apply unknown ayon settings first #5435 + +Settings of custom addons are available in converted settings. + + +___ + +
+ + +
+Maya: Fix wrong subset name of render family in deadline #5442 + +New Publisher is creating different subset names than previously which resulted in duplication of `render` string in final subset name of `render` family published on Deadline.This PR solves that, it also fixes issues with legacy instances from old publisher, it matches the subset name as was before.This solves same issue in Max implementation. + + +___ + +
+ + +
+Maya: Fix setting of version to workfile instance #5452 + +If there are multiple instances of renderlayer published, previous logic resulted in unpredictable rewrite of instance family to 'workfile' if `Sync render version with workfile` was on. + + +___ + +
+ + +
+Maya: Context plugin shouldn't be tied to family #5464 + +`Maya Current File` collector was tied to `workfile` unnecessary. It should run even if `workile` instance is not being published. + + +___ + +
+ + +
+Unreal: Fix loading hero version for static and skeletal meshes #5393 + +Fixed a problem with loading hero versions for static ans skeletal meshes. + + +___ + +
+ + +
+TVPaint: Fix 'repeat' behavior #5412 + +Calculation of frames for repeat behavior is working correctly. + + +___ + +
+ + +
+AYON: Thumbnails cache and api prep #5437 + +Moved thumbnails cache from ayon python api to OpenPype and prepare AYON thumbnail resolver for new api functions. Current implementation should work with old and new ayon-python-api. + + +___ + +
+ + +
+Nuke: Name of the Read Node should be updated correctly when switching versions or assets. #5444 + +Bug fixing of the read node's name not being updated correctly when setting version or switching asset. + + +___ + +
+ + +
+Farm publishing: asymmetric handles fixed #5446 + +Handles are now set correctly on farm published product version if asymmetric were set to shot attributes. + + +___ + +
+ + +
+Scene Inventory: Provider icons fix #5450 + +Fix how provider icons are accessed in scene inventory. + + +___ + +
+ + +
+Fix typo on Deadline OP plugin name #5453 + +Surprised that no one has hit this bug yet... but it seems like there was a typo on the name of the OP Deadline plugin when submitting jobs to it. + + +___ + +
+ + +
+AYON: Fix version attributes update #5472 + +Fixed updates of attribs in AYON mode. + + +___ + +
+ +### **Merged pull requests** + + +
+Added missing defaults for import_loader #5447 + + +___ + +
+ + +
+Bug: Local settings don't open on 3.14.7 #5220 + +### Before posting a new ticket, have you looked through the documentation to find an answer? + +Yes I have + +### Have you looked through the existing tickets to find any related issues ? + +Not yet + +### Author of the bug + +@FadyFS + +### Version + +3.15.11-nightly.3 + +### What platform you are running OpenPype on? + +Linux / Centos + +### Current Behavior: + +the previous behavior (bug) : +![image](https://github.com/quadproduction/OpenPype/assets/135602303/09bff9d5-3f8b-4339-a1e5-30c04ade828c) + + +### Expected Behavior: + +![image](https://github.com/quadproduction/OpenPype/assets/135602303/c505a103-7965-4796-bcdf-73bcc48a469b) + + +### What type of bug is it ? + +Happened only once in a particular configuration + +### Which project / workfile / asset / ... + +open settings with 3.14.7 + +### Steps To Reproduce: + +1. Run openpype on the 3.15.11-nightly.3 version +2. Open settings in 3.14.7 version + +### Relevant log output: + +_No response_ + +### Additional context: + +_No response_ + +___ + +
+ + +
+Tests: Add automated targets for tests #5443 + +Without it plugins with 'automated' targets won't be triggered (eg `CloseAE` etc.) + + +___ + +
+ + + + ## [3.16.3](https://github.com/ynput/OpenPype/tree/3.16.3) diff --git a/openpype/version.py b/openpype/version.py index 444721e19c..857a9574d8 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.16.4-nightly.3" +__version__ = "3.16.4" diff --git a/pyproject.toml b/pyproject.toml index 5e7938751e..a07c547123 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.16.3" # OpenPype +version = "3.16.4" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From ae53caacc057e77271b2fc2389362bb04bb53e14 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 22 Aug 2023 14:42:21 +0000 Subject: [PATCH 144/327] 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 326c9e8c86..5c264e4d98 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.16.4 - 3.16.4-nightly.3 - 3.16.4-nightly.2 - 3.16.4-nightly.1 @@ -134,7 +135,6 @@ body: - 3.14.8-nightly.2 - 3.14.8-nightly.1 - 3.14.7 - - 3.14.7-nightly.8 validations: required: true - type: dropdown From cd9ec2b73a269d4973db81622dbadb268ec7a24d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 22 Aug 2023 17:40:58 +0200 Subject: [PATCH 145/327] OCIO: adding exception for nuke, hiero into hook --- openpype/hooks/pre_ocio_hook.py | 3 +++ openpype/hosts/nuke/api/lib.py | 1 + 2 files changed, 4 insertions(+) diff --git a/openpype/hooks/pre_ocio_hook.py b/openpype/hooks/pre_ocio_hook.py index 1307ed9f76..add3a0adaf 100644 --- a/openpype/hooks/pre_ocio_hook.py +++ b/openpype/hooks/pre_ocio_hook.py @@ -45,6 +45,9 @@ class OCIOEnvHook(PreLaunchHook): if config_data: ocio_path = config_data["path"] + if self.host_name in ["nuke", "hiero"]: + ocio_path = ocio_path.replace("\\", "/") + self.log.info( f"Setting OCIO environment to config path: {ocio_path}") diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 4a1e109b17..2a6c1fb12c 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2177,6 +2177,7 @@ Reopening Nuke should synchronize these paths and resolve any discrepancies. """ # replace path with env var if possible ocio_path = self._replace_ocio_path_with_env_var(config_data) + ocio_path = ocio_path.replace("\\", "/") log.info("Setting OCIO config path to: `{}`".format( ocio_path)) From 99ceef33e32c14cef403ff3852b0f15f40d7807a Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 23 Aug 2023 03:24:31 +0000 Subject: [PATCH 146/327] [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 857a9574d8..f8a49f8466 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.16.4" +__version__ = "3.16.5-nightly.1" From 88f1d839f1d2186349b72c12059a51aeb7d7dd3a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 23 Aug 2023 10:07:13 +0200 Subject: [PATCH 147/327] Added super call to init (#5480) DL 10.3 requires plugin inheriting from DeadlinePlugin to call super's __init__ explicitly. --- .../repository/custom/plugins/Ayon/Ayon.py | 1 + .../HarmonyOpenPype/HarmonyOpenPype.py | 59 ++++++++++--------- .../custom/plugins/OpenPype/OpenPype.py | 3 +- .../OpenPypeTileAssembler.py | 1 + 4 files changed, 34 insertions(+), 30 deletions(-) diff --git a/openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.py b/openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.py index 16149d7e20..1544acc2a4 100644 --- a/openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.py +++ b/openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.py @@ -38,6 +38,7 @@ class AyonDeadlinePlugin(DeadlinePlugin): for publish process. """ def __init__(self): + super().__init__() self.InitializeProcessCallback += self.InitializeProcess self.RenderExecutableCallback += self.RenderExecutable self.RenderArgumentCallback += self.RenderArgument diff --git a/openpype/modules/deadline/repository/custom/plugins/HarmonyOpenPype/HarmonyOpenPype.py b/openpype/modules/deadline/repository/custom/plugins/HarmonyOpenPype/HarmonyOpenPype.py index 0615af95dd..2f6e9cf379 100644 --- a/openpype/modules/deadline/repository/custom/plugins/HarmonyOpenPype/HarmonyOpenPype.py +++ b/openpype/modules/deadline/repository/custom/plugins/HarmonyOpenPype/HarmonyOpenPype.py @@ -8,13 +8,14 @@ from Deadline.Scripting import * def GetDeadlinePlugin(): return HarmonyOpenPypePlugin() - + def CleanupDeadlinePlugin( deadlinePlugin ): deadlinePlugin.Cleanup() - + class HarmonyOpenPypePlugin( DeadlinePlugin ): def __init__( self ): + super().__init__() self.InitializeProcessCallback += self.InitializeProcess self.RenderExecutableCallback += self.RenderExecutable self.RenderArgumentCallback += self.RenderArgument @@ -24,11 +25,11 @@ class HarmonyOpenPypePlugin( DeadlinePlugin ): print("Cleanup") for stdoutHandler in self.StdoutHandlers: del stdoutHandler.HandleCallback - + del self.InitializeProcessCallback del self.RenderExecutableCallback del self.RenderArgumentCallback - + def CheckExitCode( self, exitCode ): print("check code") if exitCode != 0: @@ -36,20 +37,20 @@ class HarmonyOpenPypePlugin( DeadlinePlugin ): self.LogInfo( "Renderer reported an error with error code 100. This will be ignored, since the option to ignore it is specified in the Job Properties." ) else: self.FailRender( "Renderer returned non-zero error code %d. Check the renderer's output." % exitCode ) - + def InitializeProcess( self ): self.PluginType = PluginType.Simple self.StdoutHandling = True self.PopupHandling = True - + self.AddStdoutHandlerCallback( "Rendered frame ([0-9]+)" ).HandleCallback += self.HandleStdoutProgress - + def HandleStdoutProgress( self ): startFrame = self.GetStartFrame() endFrame = self.GetEndFrame() if( endFrame - startFrame + 1 != 0 ): self.SetProgress( 100 * ( int(self.GetRegexMatch(1)) - startFrame + 1 ) / ( endFrame - startFrame + 1 ) ) - + def RenderExecutable( self ): version = int( self.GetPluginInfoEntry( "Version" ) ) exe = "" @@ -58,7 +59,7 @@ class HarmonyOpenPypePlugin( DeadlinePlugin ): if( exe == "" ): self.FailRender( "Harmony render executable was not found in the configured separated list \"" + exeList + "\". The path to the render executable can be configured from the Plugin Configuration in the Deadline Monitor." ) return exe - + def RenderArgument( self ): renderArguments = "-batch" @@ -72,20 +73,20 @@ class HarmonyOpenPypePlugin( DeadlinePlugin ): resolutionX = self.GetIntegerPluginInfoEntryWithDefault( "ResolutionX", -1 ) resolutionY = self.GetIntegerPluginInfoEntryWithDefault( "ResolutionY", -1 ) fov = self.GetFloatPluginInfoEntryWithDefault( "FieldOfView", -1 ) - + if resolutionX > 0 and resolutionY > 0 and fov > 0: renderArguments += " -res " + str( resolutionX ) + " " + str( resolutionY ) + " " + str( fov ) - + camera = self.GetPluginInfoEntryWithDefault( "Camera", "" ) - + if not camera == "": renderArguments += " -camera " + camera - + startFrame = str( self.GetStartFrame() ) endFrame = str( self.GetEndFrame() ) - + renderArguments += " -frames " + startFrame + " " + endFrame - + if not self.GetBooleanPluginInfoEntryWithDefault( "IsDatabase", False ): sceneFilename = self.GetPluginInfoEntryWithDefault( "SceneFile", self.GetDataFilename() ) sceneFilename = RepositoryUtils.CheckPathMapping( sceneFilename ) @@ -99,12 +100,12 @@ class HarmonyOpenPypePlugin( DeadlinePlugin ): renderArguments += " -scene " + scene version = self.GetPluginInfoEntryWithDefault( "SceneVersion", "" ) renderArguments += " -version " + version - + #tempSceneDirectory = self.CreateTempDirectory( "thread" + str(self.GetThreadNumber()) ) - #preRenderScript = + #preRenderScript = rendernodeNum = 0 scriptBuilder = StringBuilder() - + while True: nodeName = self.GetPluginInfoEntryWithDefault( "Output" + str( rendernodeNum ) + "Node", "" ) if nodeName == "": @@ -115,35 +116,35 @@ class HarmonyOpenPypePlugin( DeadlinePlugin ): nodeLeadingZero = self.GetPluginInfoEntryWithDefault( "Output" + str( rendernodeNum ) + "LeadingZero", "" ) nodeFormat = self.GetPluginInfoEntryWithDefault( "Output" + str( rendernodeNum ) + "Format", "" ) nodeStartFrame = self.GetPluginInfoEntryWithDefault( "Output" + str( rendernodeNum ) + "StartFrame", "" ) - + if not nodePath == "": scriptBuilder.AppendLine("node.setTextAttr( \"" + nodeName + "\", \"drawingName\", 1, \"" + nodePath + "\" );") - + if not nodeLeadingZero == "": scriptBuilder.AppendLine("node.setTextAttr( \"" + nodeName + "\", \"leadingZeros\", 1, \"" + nodeLeadingZero + "\" );") - + if not nodeFormat == "": scriptBuilder.AppendLine("node.setTextAttr( \"" + nodeName + "\", \"drawingType\", 1, \"" + nodeFormat + "\" );") - + if not nodeStartFrame == "": scriptBuilder.AppendLine("node.setTextAttr( \"" + nodeName + "\", \"start\", 1, \"" + nodeStartFrame + "\" );") - + if nodeType == "Movie": nodePath = self.GetPluginInfoEntryWithDefault( "Output" + str( rendernodeNum ) + "Path", "" ) if not nodePath == "": scriptBuilder.AppendLine("node.setTextAttr( \"" + nodeName + "\", \"moviePath\", 1, \"" + nodePath + "\" );") - + rendernodeNum += 1 - + tempDirectory = self.CreateTempDirectory( "thread" + str(self.GetThreadNumber()) ) preRenderScriptName = Path.Combine( tempDirectory, "preRenderScript.txt" ) - + File.WriteAllText( preRenderScriptName, scriptBuilder.ToString() ) - + preRenderInlineScript = self.GetPluginInfoEntryWithDefault( "PreRenderInlineScript", "" ) if preRenderInlineScript: renderArguments += " -preRenderInlineScript \"" + preRenderInlineScript +"\"" - + renderArguments += " -preRenderScript \"" + preRenderScriptName +"\"" - + return renderArguments diff --git a/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.py b/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.py index 6e1b973fb9..004c58d346 100644 --- a/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.py +++ b/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.py @@ -38,6 +38,7 @@ class OpenPypeDeadlinePlugin(DeadlinePlugin): for publish process. """ def __init__(self): + super().__init__() self.InitializeProcessCallback += self.InitializeProcess self.RenderExecutableCallback += self.RenderExecutable self.RenderArgumentCallback += self.RenderArgument @@ -107,7 +108,7 @@ class OpenPypeDeadlinePlugin(DeadlinePlugin): "Scanning for compatible requested " f"version {requested_version}")) dir_list = self.GetConfigEntry("OpenPypeInstallationDirs") - + # clean '\ ' for MacOS pasting if platform.system().lower() == "darwin": dir_list = dir_list.replace("\\ ", " ") diff --git a/openpype/modules/deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.py b/openpype/modules/deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.py index b51daffbc8..9641c16d20 100644 --- a/openpype/modules/deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.py +++ b/openpype/modules/deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.py @@ -249,6 +249,7 @@ class OpenPypeTileAssembler(DeadlinePlugin): def __init__(self): """Init.""" + super().__init__() self.InitializeProcessCallback += self.initialize_process self.RenderExecutableCallback += self.render_executable self.RenderArgumentCallback += self.render_argument From 83508a93eadb1387a49dca465d68bcf0d96badc6 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 23 Aug 2023 13:45:20 +0200 Subject: [PATCH 148/327] nuke: fixing thumbnail and monitor out root attributes --- openpype/hosts/nuke/api/lib.py | 54 +++++++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 4a1e109b17..cedbe6d5e6 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2041,6 +2041,7 @@ class WorkfileSettings(object): ) workfile_settings = imageio_host["workfile"] + viewer_process_settings = imageio_host["viewer"]["viewerProcess"] if not config_data: # TODO: backward compatibility for old projects - remove later @@ -2091,6 +2092,31 @@ class WorkfileSettings(object): workfile_settings.pop("colorManagement", None) workfile_settings.pop("OCIO_config", None) + # treat monitor lut separately + monitor_lut = workfile_settings.pop("monitorLut", None) + m_display, m_viewer = get_viewer_config_from_string(monitor_lut) + v_display, v_viewer = get_viewer_config_from_string( + viewer_process_settings + ) + + # set monitor lut differently for nuke version 14 + if nuke.NUKE_VERSION_MAJOR >= 14: + workfile_settings["monitorOutLUT"] = create_viewer_profile_string( + m_viewer, m_display, path_like=False) + # monitorLut=thumbnails - viewerProcess makes more sense + workfile_settings["monitorLut"] = create_viewer_profile_string( + v_viewer, v_display, path_like=False) + + if nuke.NUKE_VERSION_MAJOR == 13: + workfile_settings["monitorOutLUT"] = create_viewer_profile_string( + m_viewer, m_display, path_like=False) + # monitorLut=thumbnails - viewerProcess makes more sense + workfile_settings["monitorLut"] = create_viewer_profile_string( + v_viewer, v_display, path_like=True) + if nuke.NUKE_VERSION_MAJOR <= 12: + workfile_settings["monitorLut"] = create_viewer_profile_string( + m_viewer, m_display, path_like=True) + # then set the rest for knob, value_ in workfile_settings.items(): # skip unfilled ocio config path @@ -3320,11 +3346,11 @@ def get_viewer_config_from_string(input_string): display = split[0] elif "(" in viewer: pattern = r"([\w\d\s\.\-]+).*[(](.*)[)]" - result = re.findall(pattern, viewer) + result_ = re.findall(pattern, viewer) try: - result = result.pop() - display = str(result[1]).rstrip() - viewer = str(result[0]).rstrip() + result_ = result_.pop() + display = str(result_[1]).rstrip() + viewer = str(result_[0]).rstrip() except IndexError: raise IndexError(( "Viewer Input string is not correct. " @@ -3332,3 +3358,23 @@ def get_viewer_config_from_string(input_string): ).format(input_string)) return (display, viewer) + + +def create_viewer_profile_string(viewer, display=None, path_like=False): + """Convert viewer and display to string + + Args: + viewer (str): viewer name + display (Optional[str]): display name + path_like (Optional[bool]): if True, return path like string + + Returns: + str: viewer config string + """ + if display: + if path_like: + return "{}/{}".format(display, viewer) + else: + return "{} ({})".format(viewer, display) + else: + return viewer From 0f8cc0301fd2e44259c524eefe39b7db733fecaa Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 23 Aug 2023 14:47:10 +0200 Subject: [PATCH 149/327] Nuke: fixing missing `instance_id` --- openpype/hosts/nuke/api/pipeline.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index 65b4b91323..a1d290646c 100644 --- a/openpype/hosts/nuke/api/pipeline.py +++ b/openpype/hosts/nuke/api/pipeline.py @@ -543,6 +543,9 @@ def list_instances(creator_id=None): For SubsetManager + Args: + creator_id (Optional[str]): creator identifier + Returns: (list) of dictionaries matching instances format """ @@ -575,10 +578,13 @@ def list_instances(creator_id=None): if creator_id and instance_data["creator_identifier"] != creator_id: continue - if instance_data["instance_id"] in instance_ids: + instance_id = instance_data.get("instance_id") + if not instance_id: + pass + elif instance_id in instance_ids: instance_data.pop("instance_id") else: - instance_ids.add(instance_data["instance_id"]) + instance_ids.add(instance_id) # node name could change, so update subset name data _update_subset_name_data(instance_data, node) From 3603fdfe0012f905634d1786275a2f72b4ca2203 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 23 Aug 2023 15:11:26 +0200 Subject: [PATCH 150/327] Nuke: existing frames validator is repairing render target --- .../publish/validate_rendered_frames.py | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py b/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py index 45c20412c8..ef3d4d0bb5 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py +++ b/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py @@ -14,19 +14,28 @@ class RepairActionBase(pyblish.api.Action): # Get the errored instances return get_errored_instances_from_context(context, plugin=plugin) - def repair_knob(self, instances, state): + def repair_knob(self, context, instances, state): + create_context = context.data["create_context"] for instance in instances: - node = instance.data["transientData"]["node"] - files_remove = [os.path.join(instance.data["outputDir"], f) - for r in instance.data.get("representations", []) - for f in r.get("files", []) - ] + files_remove = [ + os.path.join(instance.data["outputDir"], f_) + for r_ in instance.data.get("representations", []) + for f_ in r_.get("files", []) + ] self.log.info("Files to be removed: {}".format(files_remove)) - for f in files_remove: - os.remove(f) - self.log.debug("removing file: {}".format(f)) - node["render"].setValue(state) + for f_ in files_remove: + os.remove(f_) + self.log.debug("removing file: {}".format(f_)) + + # Reset the render knob + instance_id = instance.data["instance_id"] + created_instance = create_context.get_instance_by_id( + instance_id + ) + created_instance.creator_attributes["render_target"] = state + self.log.info("Rendering toggled to `{}`".format(state)) + create_context.save_changes() class RepairCollectionActionToLocal(RepairActionBase): @@ -34,7 +43,7 @@ class RepairCollectionActionToLocal(RepairActionBase): def process(self, context, plugin): instances = self.get_instance(context, plugin) - self.repair_knob(instances, "Local") + self.repair_knob(context, instances, "local") class RepairCollectionActionToFarm(RepairActionBase): @@ -42,7 +51,7 @@ class RepairCollectionActionToFarm(RepairActionBase): def process(self, context, plugin): instances = self.get_instance(context, plugin) - self.repair_knob(instances, "On farm") + self.repair_knob(context, instances, "farm") class ValidateRenderedFrames(pyblish.api.InstancePlugin): From ed5c299c515b46bd0efc1da705bb484746688370 Mon Sep 17 00:00:00 2001 From: Libor Batek Date: Wed, 23 Aug 2023 15:59:45 +0200 Subject: [PATCH 151/327] added UE to extract burnins families --- openpype/plugins/publish/extract_burnin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/extract_burnin.py b/openpype/plugins/publish/extract_burnin.py index 4a64711bfd..e5b37ee3b4 100644 --- a/openpype/plugins/publish/extract_burnin.py +++ b/openpype/plugins/publish/extract_burnin.py @@ -53,8 +53,8 @@ class ExtractBurnin(publish.Extractor): "flame", "houdini", "max", - "blender" - # "resolve" + "blender", + "unreal" ] optional = True From 6021a43ab0255d6410d9c307f1507c166d81aa9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Wed, 23 Aug 2023 16:39:21 +0200 Subject: [PATCH 152/327] Update openpype/hosts/nuke/plugins/publish/collect_writes.py Co-authored-by: Roy Nieterau --- openpype/hosts/nuke/plugins/publish/collect_writes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/collect_writes.py b/openpype/hosts/nuke/plugins/publish/collect_writes.py index f1b36ba886..6f9245f5b9 100644 --- a/openpype/hosts/nuke/plugins/publish/collect_writes.py +++ b/openpype/hosts/nuke/plugins/publish/collect_writes.py @@ -14,7 +14,7 @@ class CollectNukeWrites(pyblish.api.InstancePlugin, hosts = ["nuke", "nukeassist"] families = ["render", "prerender", "image"] - # cashing + # cache _write_nodes = {} _frame_ranges = {} From fd2ba5a07fca076ecf6b2b6793b0f266ce9e4f47 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 23 Aug 2023 16:48:51 +0200 Subject: [PATCH 153/327] empty line --- .../hosts/nuke/plugins/publish/collect_nuke_instance_data.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/collect_nuke_instance_data.py b/openpype/hosts/nuke/plugins/publish/collect_nuke_instance_data.py index edd7a5cf27..b0f69e8ab8 100644 --- a/openpype/hosts/nuke/plugins/publish/collect_nuke_instance_data.py +++ b/openpype/hosts/nuke/plugins/publish/collect_nuke_instance_data.py @@ -51,6 +51,5 @@ class CollectNukeInstanceData(pyblish.api.InstancePlugin): if instance.data.get("review"): instance.data["families"].append("review") - self.log.debug("Collected instance: {}".format( instance.data)) From fe5dc20877355a8b23a54c359dedca7111e6a00e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 23 Aug 2023 17:31:46 +0200 Subject: [PATCH 154/327] traypublisher: adding preset for audio product --- .../project_settings/traypublisher.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/openpype/settings/defaults/project_settings/traypublisher.json b/openpype/settings/defaults/project_settings/traypublisher.json index dda958ebcd..7f7b7d1452 100644 --- a/openpype/settings/defaults/project_settings/traypublisher.json +++ b/openpype/settings/defaults/project_settings/traypublisher.json @@ -256,6 +256,23 @@ "allow_multiple_items": true, "allow_version_control": false, "extensions": [] + }, + { + "family": "audio", + "identifier": "", + "label": "Audio ", + "icon": "fa5s.file-audio", + "default_variants": [ + "Main" + ], + "description": "Audio product", + "detailed_description": "Audio files for review or final delivery", + "allow_sequences": false, + "allow_multiple_items": false, + "allow_version_control": false, + "extensions": [ + ".wav" + ] } ], "editorial_creators": { From fa80317f6a6fb8ac5f65d11304444cf128b0567a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 24 Aug 2023 17:08:37 +0800 Subject: [PATCH 155/327] namespace fix for most loaders except alembic loaders --- openpype/hosts/max/api/lib.py | 9 ++++ .../hosts/max/plugins/load/load_camera_fbx.py | 32 ++++++++----- .../hosts/max/plugins/load/load_max_scene.py | 48 +++++++++---------- openpype/hosts/max/plugins/load/load_model.py | 7 +++ .../hosts/max/plugins/load/load_model_fbx.py | 43 ++++++++++------- .../hosts/max/plugins/load/load_model_obj.py | 38 ++++++++------- .../hosts/max/plugins/load/load_model_usd.py | 43 ++++++++++------- .../hosts/max/plugins/load/load_pointcache.py | 8 ++++ .../hosts/max/plugins/load/load_pointcloud.py | 24 ++++++---- .../max/plugins/load/load_redshift_proxy.py | 26 +++++----- 10 files changed, 168 insertions(+), 110 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index e357080cbc..08819ba155 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -368,3 +368,12 @@ def unique_namespace(namespace, format="%02d", else: increment_version = True iteration += 1 + + +def get_namespace(container_name): + node = rt.getNodeByName(container_name) + if not node: + raise RuntimeError("Master Container Not Found..") + name = rt.getUserProp(node, "name") + namespace = rt.getUserProp(node, "namespace") + return namespace, name diff --git a/openpype/hosts/max/plugins/load/load_camera_fbx.py b/openpype/hosts/max/plugins/load/load_camera_fbx.py index c0e1172a6d..c70ece6293 100644 --- a/openpype/hosts/max/plugins/load/load_camera_fbx.py +++ b/openpype/hosts/max/plugins/load/load_camera_fbx.py @@ -1,7 +1,9 @@ import os from openpype.hosts.max.api import lib, maintained_selection -from openpype.hosts.max.api.lib import unique_namespace +from openpype.hosts.max.api.lib import ( + unique_namespace, get_namespace +) from openpype.hosts.max.api.pipeline import ( containerise, import_custom_attribute_data, @@ -33,16 +35,17 @@ class FbxLoader(load.LoaderPlugin): rt.name("noPrompt"), using=rt.FBXIMP) - container = rt.container(name=name) - selections = rt.GetCurrentSelection() - import_custom_attribute_data(container, selections) - for selection in selections: - selection.Parent = container - namespace = unique_namespace( name + "_", suffix="_", ) + container = rt.container(name=f"{namespace}:{name}") + selections = rt.GetCurrentSelection() + import_custom_attribute_data(container, selections) + + for selection in selections: + selection.Parent = container + selection.name = f"{namespace}:{selection.name}" return containerise( name, [container], context, @@ -54,11 +57,13 @@ class FbxLoader(load.LoaderPlugin): path = get_representation_path(representation) node_name = container["instance_node"] node = rt.getNodeByName(node_name) - container_name = node_name.split(":")[-1] - param_container, _ = container_name.split("_") - - inst_container = rt.getNodeByName(param_container) - rt.Select(node.Children) + namespace, name = get_namespace(node_name) + sub_node_name = f"{namespace}:{name}" + inst_container = rt.getNodeByName(sub_node_name) + rt.Select(inst_container.Children) + for prev_fbx_obj in rt.selection: + if rt.isValidNode(prev_fbx_obj): + rt.Delete(prev_fbx_obj) rt.FBXImporterSetParam("Animation", True) rt.FBXImporterSetParam("Camera", True) @@ -71,10 +76,11 @@ class FbxLoader(load.LoaderPlugin): for fbx_object in current_fbx_objects: if fbx_object.Parent != inst_container: fbx_object.Parent = inst_container + fbx_object.name = f"{namespace}:{fbx_object.name}" for children in node.Children: if rt.classOf(children) == rt.Container: - if children.name == param_container: + if children.name == sub_node_name: update_custom_attribute_data( children, current_fbx_objects) diff --git a/openpype/hosts/max/plugins/load/load_max_scene.py b/openpype/hosts/max/plugins/load/load_max_scene.py index aa177291d8..cf5f7736e3 100644 --- a/openpype/hosts/max/plugins/load/load_max_scene.py +++ b/openpype/hosts/max/plugins/load/load_max_scene.py @@ -1,7 +1,9 @@ import os from openpype.hosts.max.api import lib -from openpype.hosts.max.api.lib import unique_namespace +from openpype.hosts.max.api.lib import ( + unique_namespace, get_namespace +) from openpype.hosts.max.api.pipeline import ( containerise, import_custom_attribute_data, update_custom_attribute_data @@ -29,17 +31,21 @@ class MaxSceneLoader(load.LoaderPlugin): path = path.replace('\\', '/') rt.MergeMaxFile(path, quiet=True, includeFullGroup=True) max_objects = rt.getLastMergedNodes() + max_object_names = [obj.name for obj in max_objects] # implement the OP/AYON custom attributes before load max_container = [] - container = rt.Container(name=name) - import_custom_attribute_data(container, max_objects) - max_container.append(container) - max_container.extend(max_objects) namespace = unique_namespace( name + "_", suffix="_", ) + container_name = f"{namespace}:{name}" + container = rt.Container(name=container_name) + import_custom_attribute_data(container, max_objects) + max_container.append(container) + max_container.extend(max_objects) + for max_obj, obj_name in zip(max_objects, max_object_names): + max_obj.name = f"{namespace}:{obj_name}" return containerise( name, max_container, context, namespace, loader=self.__class__.__name__) @@ -51,34 +57,28 @@ class MaxSceneLoader(load.LoaderPlugin): node_name = container["instance_node"] node = rt.getNodeByName(node_name) - container_name = node_name.split(":")[-1] - param_container, _ = container_name.split("_") - + namespace, name = get_namespace(node_name) + sub_container_name = f"{namespace}:{name}" # delete the old container with attribute # delete old duplicate - prev_max_object_names = [obj.name for obj - in rt.getLastMergedNodes()] + #TODO: get the prev_max_objects by using node.Children + rt.Select(node.Children) + for prev_max_obj in rt.GetCurrentSelection(): + if rt.isValidNode(prev_max_obj) and prev_max_obj.name != sub_container_name: # noqa + rt.Delete(prev_max_obj) rt.MergeMaxFile(path, rt.Name("deleteOldDups")) current_max_objects = rt.getLastMergedNodes() current_max_object_names = [obj.name for obj in current_max_objects] - for name in current_max_object_names: - idx = rt.findItem(prev_max_object_names, name) - if idx: - prev_max_object_names = rt.deleteItem( - prev_max_object_names, idx) - for object_name in prev_max_object_names: - prev_max_object = rt.getNodeByName(object_name) - rt.Delete(prev_max_object) - + sub_container = rt.getNodeByName(sub_container_name) + update_custom_attribute_data(sub_container, current_max_objects) for max_object in current_max_objects: max_object.Parent = node - for children in node.Children: - if rt.classOf(children) == rt.Container: - if children.name == param_container: - update_custom_attribute_data( - children, current_max_objects) + for max_obj, obj_name in zip( + current_max_objects, current_max_object_names): + max_obj.name = f"{namespace}:{obj_name}" + lib.imprint(container["instance_node"], { "representation": str(representation["_id"]) diff --git a/openpype/hosts/max/plugins/load/load_model.py b/openpype/hosts/max/plugins/load/load_model.py index deb3389992..aee948f2e2 100644 --- a/openpype/hosts/max/plugins/load/load_model.py +++ b/openpype/hosts/max/plugins/load/load_model.py @@ -58,6 +58,13 @@ class ModelAbcLoader(load.LoaderPlugin): name + "_", suffix="_", ) + for abc_object in abc_container.Children: + abc_object.name = f"{namespace}:{abc_object.name}" + # rename the abc container with namespace + abc_container_name = f"{namespace}:{name}" + abc_container.name = abc_container_name + # get the correct container + abc_container = rt.GetNodeByName(abc_container_name) return containerise( name, [abc_container], context, diff --git a/openpype/hosts/max/plugins/load/load_model_fbx.py b/openpype/hosts/max/plugins/load/load_model_fbx.py index f85bfa03a1..6097a4ca6e 100644 --- a/openpype/hosts/max/plugins/load/load_model_fbx.py +++ b/openpype/hosts/max/plugins/load/load_model_fbx.py @@ -5,7 +5,9 @@ from openpype.hosts.max.api.pipeline import ( update_custom_attribute_data ) from openpype.hosts.max.api import lib -from openpype.hosts.max.api.lib import unique_namespace +from openpype.hosts.max.api.lib import ( + unique_namespace, get_namespace +) from openpype.hosts.max.api.lib import maintained_selection @@ -28,20 +30,18 @@ class FbxModelLoader(load.LoaderPlugin): rt.FBXImporterSetParam("Preserveinstances", True) rt.importFile(filepath, rt.name("noPrompt"), using=rt.FBXIMP) - container = rt.GetNodeByName(name) - - container = rt.Container(name=name) - + namespace = unique_namespace( + name + "_", + suffix="_", + ) + container = rt.container(name=f"{namespace}:{name}") selections = rt.GetCurrentSelection() import_custom_attribute_data(container, selections) for selection in selections: selection.Parent = container + selection.name = f"{namespace}:{selection.name}" - namespace = unique_namespace( - name + "_", - suffix="_", - ) return containerise( name, [container], context, namespace, loader=self.__class__.__name__ @@ -52,8 +52,14 @@ class FbxModelLoader(load.LoaderPlugin): path = get_representation_path(representation) node_name = container["instance_node"] node = rt.getNodeByName(node_name) - container_name = node_name.split(":")[-1] - param_container, _ = container_name.split("_") + namespace, name = get_namespace(node_name) + sub_node_name = f"{namespace}:{name}" + inst_container = rt.getNodeByName(sub_node_name) + rt.Select(inst_container.Children) + for prev_fbx_obj in rt.selection: + if rt.isValidNode(prev_fbx_obj): + rt.Delete(prev_fbx_obj) + rt.FBXImporterSetParam("Animation", False) rt.FBXImporterSetParam("Cameras", False) rt.FBXImporterSetParam("Mode", rt.Name("merge")) @@ -61,16 +67,17 @@ class FbxModelLoader(load.LoaderPlugin): rt.FBXImporterSetParam("Preserveinstances", True) rt.importFile(path, rt.name("noPrompt"), using=rt.FBXIMP) current_fbx_objects = rt.GetCurrentSelection() - - inst_container = rt.getNodeByName(param_container) - for children in node.Children: - if rt.classOf(children) == rt.Container: - if children.name == param_container: - update_custom_attribute_data( - children, current_fbx_objects) for fbx_object in current_fbx_objects: if fbx_object.Parent != inst_container: fbx_object.Parent = inst_container + fbx_object.name = f"{namespace}:{fbx_object.name}" + + for children in node.Children: + if rt.classOf(children) == rt.Container: + if children.name == sub_node_name: + update_custom_attribute_data( + children, current_fbx_objects) + with maintained_selection(): rt.Select(node) diff --git a/openpype/hosts/max/plugins/load/load_model_obj.py b/openpype/hosts/max/plugins/load/load_model_obj.py index b42ef399b0..225801b8d0 100644 --- a/openpype/hosts/max/plugins/load/load_model_obj.py +++ b/openpype/hosts/max/plugins/load/load_model_obj.py @@ -1,7 +1,9 @@ import os from openpype.hosts.max.api import lib -from openpype.hosts.max.api.lib import unique_namespace +from openpype.hosts.max.api.lib import ( + unique_namespace, get_namespace +) from openpype.hosts.max.api.lib import maintained_selection from openpype.hosts.max.api.pipeline import ( containerise, @@ -27,18 +29,19 @@ class ObjLoader(load.LoaderPlugin): self.log.debug("Executing command to import..") rt.Execute(f'importFile @"{filepath}" #noPrompt using:ObjImp') - # create "missing" container for obj import - container = rt.Container(name=name) - selections = rt.GetCurrentSelection() - import_custom_attribute_data(container, selections) - # get current selection - for selection in selections: - selection.Parent = container namespace = unique_namespace( name + "_", suffix="_", ) + # create "missing" container for obj import + container = rt.Container(name=f"{namespace}:{name}") + selections = rt.GetCurrentSelection() + import_custom_attribute_data(container, selections) + # get current selection + for selection in selections: + selection.Parent = container + selection.name = f"{namespace}:{selection.name}" return containerise( name, [container], context, namespace, loader=self.__class__.__name__) @@ -48,21 +51,22 @@ class ObjLoader(load.LoaderPlugin): path = get_representation_path(representation) node_name = container["instance_node"] - node = rt.GetNodeByName(node_name) - - container_name = node_name.split(":")[-1] - param_container, _ = container_name.split("_") - - inst_container = rt.getNodeByName(param_container) - for child in inst_container.Children: - rt.Delete(child) + node = rt.getNodeByName(node_name) + namespace, name = get_namespace(node_name) + sub_node_name = f"{namespace}:{name}" + inst_container = rt.getNodeByName(sub_node_name) + rt.Select(inst_container.Children) + for prev_obj in rt.selection: + if rt.isValidNode(prev_obj): + rt.Delete(prev_obj) rt.Execute(f'importFile @"{path}" #noPrompt using:ObjImp') # get current selection selections = rt.GetCurrentSelection() + update_custom_attribute_data(inst_container, selections) for selection in selections: selection.Parent = inst_container - update_custom_attribute_data(inst_container, selections) + selection.name = f"{namespace}:{selection.name}" with maintained_selection(): rt.Select(node) diff --git a/openpype/hosts/max/plugins/load/load_model_usd.py b/openpype/hosts/max/plugins/load/load_model_usd.py index 4febba216e..0c17736739 100644 --- a/openpype/hosts/max/plugins/load/load_model_usd.py +++ b/openpype/hosts/max/plugins/load/load_model_usd.py @@ -1,12 +1,13 @@ import os from openpype.hosts.max.api import lib -from openpype.hosts.max.api.lib import unique_namespace +from openpype.hosts.max.api.lib import ( + unique_namespace, get_namespace +) from openpype.hosts.max.api.lib import maintained_selection from openpype.hosts.max.api.pipeline import ( containerise, - import_custom_attribute_data, - update_custom_attribute_data + import_custom_attribute_data ) from openpype.pipeline import get_representation_path, load @@ -35,14 +36,20 @@ class ModelUSDLoader(load.LoaderPlugin): rt.LogLevel = rt.Name("info") rt.USDImporter.importFile(filepath, importOptions=import_options) - asset = rt.GetNodeByName(name) - - import_custom_attribute_data(asset, asset.Children) - namespace = unique_namespace( name + "_", suffix="_", ) + asset = rt.GetNodeByName(name) + import_custom_attribute_data(asset, asset.Children) + for usd_asset in asset.Children: + usd_asset.name = f"{namespace}:{usd_asset.name}" + + asset_name = f"{namespace}:{name}" + asset.name = asset_name + # need to get the correct container after renamed + asset = rt.GetNodeByName(asset_name) + return containerise( name, [asset], context, @@ -54,12 +61,14 @@ class ModelUSDLoader(load.LoaderPlugin): path = get_representation_path(representation) node_name = container["instance_node"] node = rt.GetNodeByName(node_name) + namespace, name = get_namespace(node_name) + sub_node_name = f"{namespace}:{name}" for n in node.Children: - for r in n.Children: - rt.Delete(r) + rt.Select(n.Children) + for prev_usd_asset in rt.selection: + if rt.isValidNode(prev_usd_asset): + rt.Delete(prev_usd_asset) rt.Delete(n) - container_name = node_name.split(":")[-1] - param_container, _ = container_name.split("_") import_options = rt.USDImporter.CreateOptions() base_filename = os.path.basename(path) @@ -71,13 +80,13 @@ class ModelUSDLoader(load.LoaderPlugin): rt.USDImporter.importFile( path, importOptions=import_options) - asset = rt.GetNodeByName(param_container) + asset = rt.GetNodeByName(name) asset.Parent = node - for children in node.Children: - if rt.classOf(children) == rt.Container: - if children.name == param_container: - update_custom_attribute_data( - asset, asset.Children) + import_custom_attribute_data(asset, asset.Children) + for children in asset.Children: + children.name = f"{namespace}:{children.name}" + asset.name = sub_node_name + with maintained_selection(): rt.Select(node) diff --git a/openpype/hosts/max/plugins/load/load_pointcache.py b/openpype/hosts/max/plugins/load/load_pointcache.py index af03e70236..ca833a383c 100644 --- a/openpype/hosts/max/plugins/load/load_pointcache.py +++ b/openpype/hosts/max/plugins/load/load_pointcache.py @@ -65,6 +65,14 @@ class AbcLoader(load.LoaderPlugin): suffix="_", ) + for abc_object in abc_container.Children: + abc_object.name = f"{namespace}:{abc_object.name}" + # rename the abc container with namespace + abc_container_name = f"{namespace}:{name}" + abc_container.name = abc_container_name + # get the correct container + abc_container = rt.GetNodeByName(abc_container_name) + return containerise( name, [abc_container], context, namespace, loader=self.__class__.__name__ diff --git a/openpype/hosts/max/plugins/load/load_pointcloud.py b/openpype/hosts/max/plugins/load/load_pointcloud.py index 6c94fb7847..87b7fce292 100644 --- a/openpype/hosts/max/plugins/load/load_pointcloud.py +++ b/openpype/hosts/max/plugins/load/load_pointcloud.py @@ -1,7 +1,9 @@ import os from openpype.hosts.max.api import lib, maintained_selection -from openpype.hosts.max.api.lib import unique_namespace +from openpype.hosts.max.api.lib import ( + unique_namespace, get_namespace +) from openpype.hosts.max.api.pipeline import ( containerise, import_custom_attribute_data, @@ -26,14 +28,15 @@ class PointCloudLoader(load.LoaderPlugin): filepath = os.path.normpath(self.filepath_from_context(context)) obj = rt.tyCache() obj.filename = filepath - prt_container = rt.Container(name=name) - obj.Parent = prt_container - import_custom_attribute_data(prt_container, [obj]) namespace = unique_namespace( name + "_", suffix="_", ) + prt_container = rt.Container(name=f"{namespace}:{name}") + import_custom_attribute_data(prt_container, [obj]) + obj.Parent = prt_container + obj.name = f"{namespace}:{obj.name}" return containerise( name, [prt_container], context, @@ -45,14 +48,15 @@ class PointCloudLoader(load.LoaderPlugin): path = get_representation_path(representation) node = rt.GetNodeByName(container["instance_node"]) + namespace, name = get_namespace(container["instance_node"]) + sub_node_name = f"{namespace}:{name}" + inst_container = rt.getNodeByName(sub_node_name) + update_custom_attribute_data( + inst_container, inst_container.Children) with maintained_selection(): rt.Select(node.Children) - for sub_node in rt.Selection: - children_node = sub_node.Children - update_custom_attribute_data( - sub_node, sub_node.Children) - for prt in children_node: - prt.filename = path + for prt in inst_container.Children: + prt.filename = path lib.imprint(container["instance_node"], { "representation": str(representation["_id"]) }) diff --git a/openpype/hosts/max/plugins/load/load_redshift_proxy.py b/openpype/hosts/max/plugins/load/load_redshift_proxy.py index 1c4cd02143..a64bfa7de2 100644 --- a/openpype/hosts/max/plugins/load/load_redshift_proxy.py +++ b/openpype/hosts/max/plugins/load/load_redshift_proxy.py @@ -11,7 +11,9 @@ from openpype.hosts.max.api.pipeline import ( update_custom_attribute_data ) from openpype.hosts.max.api import lib -from openpype.hosts.max.api.lib import unique_namespace +from openpype.hosts.max.api.lib import ( + unique_namespace, get_namespace +) class RedshiftProxyLoader(load.LoaderPlugin): @@ -35,14 +37,15 @@ class RedshiftProxyLoader(load.LoaderPlugin): if collections: rs_proxy.is_sequence = True - container = rt.Container(name=name) - rs_proxy.Parent = container - import_custom_attribute_data(container, [rs_proxy]) namespace = unique_namespace( name + "_", suffix="_", ) + container = rt.Container(name=f"{namespace}:{name}") + rs_proxy.Parent = container + rs_proxy.name = f"{namespace}:{rs_proxy.name}" + import_custom_attribute_data(container, [rs_proxy]) return containerise( name, [container], context, @@ -52,13 +55,14 @@ class RedshiftProxyLoader(load.LoaderPlugin): from pymxs import runtime as rt path = get_representation_path(representation) - node = rt.getNodeByName(container["instance_node"]) - for sub_node in node.Children: - children_node = sub_node.Children - update_custom_attribute_data( - sub_node, children_node) - for proxy in children_node: - proxy.file = path + namespace, name = get_namespace(container["instance_node"]) + sub_node_name = f"{namespace}:{name}" + inst_container = rt.getNodeByName(sub_node_name) + + update_custom_attribute_data( + inst_container, inst_container.Children) + for proxy in inst_container.Children: + proxy.file = path lib.imprint(container["instance_node"], { "representation": str(representation["_id"]) From 637c6396cad3bfe85f8537d969232986694f9af4 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 24 Aug 2023 17:13:38 +0800 Subject: [PATCH 156/327] hound --- openpype/hosts/max/plugins/load/load_max_scene.py | 5 ++--- openpype/hosts/max/plugins/load/load_redshift_proxy.py | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_max_scene.py b/openpype/hosts/max/plugins/load/load_max_scene.py index cf5f7736e3..fada871c6d 100644 --- a/openpype/hosts/max/plugins/load/load_max_scene.py +++ b/openpype/hosts/max/plugins/load/load_max_scene.py @@ -61,7 +61,6 @@ class MaxSceneLoader(load.LoaderPlugin): sub_container_name = f"{namespace}:{name}" # delete the old container with attribute # delete old duplicate - #TODO: get the prev_max_objects by using node.Children rt.Select(node.Children) for prev_max_obj in rt.GetCurrentSelection(): if rt.isValidNode(prev_max_obj) and prev_max_obj.name != sub_container_name: # noqa @@ -75,8 +74,8 @@ class MaxSceneLoader(load.LoaderPlugin): update_custom_attribute_data(sub_container, current_max_objects) for max_object in current_max_objects: max_object.Parent = node - for max_obj, obj_name in zip( - current_max_objects, current_max_object_names): + for max_obj, obj_name in zip(current_max_objects, + current_max_object_names): max_obj.name = f"{namespace}:{obj_name}" diff --git a/openpype/hosts/max/plugins/load/load_redshift_proxy.py b/openpype/hosts/max/plugins/load/load_redshift_proxy.py index a64bfa7de2..b240714314 100644 --- a/openpype/hosts/max/plugins/load/load_redshift_proxy.py +++ b/openpype/hosts/max/plugins/load/load_redshift_proxy.py @@ -37,7 +37,6 @@ class RedshiftProxyLoader(load.LoaderPlugin): if collections: rs_proxy.is_sequence = True - namespace = unique_namespace( name + "_", suffix="_", From d0857a63e0a9c107fdd9158c032b67afcd70d941 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 24 Aug 2023 17:27:52 +0800 Subject: [PATCH 157/327] alembic loader namespace fix --- openpype/hosts/max/plugins/load/load_model.py | 2 -- openpype/hosts/max/plugins/load/load_pointcache.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_model.py b/openpype/hosts/max/plugins/load/load_model.py index aee948f2e2..acc2a4032b 100644 --- a/openpype/hosts/max/plugins/load/load_model.py +++ b/openpype/hosts/max/plugins/load/load_model.py @@ -63,8 +63,6 @@ class ModelAbcLoader(load.LoaderPlugin): # rename the abc container with namespace abc_container_name = f"{namespace}:{name}" abc_container.name = abc_container_name - # get the correct container - abc_container = rt.GetNodeByName(abc_container_name) return containerise( name, [abc_container], context, diff --git a/openpype/hosts/max/plugins/load/load_pointcache.py b/openpype/hosts/max/plugins/load/load_pointcache.py index ca833a383c..64bf7ddac0 100644 --- a/openpype/hosts/max/plugins/load/load_pointcache.py +++ b/openpype/hosts/max/plugins/load/load_pointcache.py @@ -70,8 +70,6 @@ class AbcLoader(load.LoaderPlugin): # rename the abc container with namespace abc_container_name = f"{namespace}:{name}" abc_container.name = abc_container_name - # get the correct container - abc_container = rt.GetNodeByName(abc_container_name) return containerise( name, [abc_container], context, From 93f897c780d5b6bda9ee56e013b12ee7b0ba6952 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 24 Aug 2023 13:59:34 +0200 Subject: [PATCH 158/327] deadline: adding collect farm target and removing redundant key --- .../plugins/publish/collect_render.py | 1 - .../fusion/plugins/publish/collect_render.py | 1 - .../plugins/publish/collect_farm_render.py | 1 - .../plugins/publish/submit_maya_muster.py | 1 - .../publish/submit_celaction_deadline.py | 2 +- .../publish/submit_houdini_render_deadline.py | 1 - .../plugins/publish/submit_max_deadline.py | 1 - .../plugins/publish/submit_maya_deadline.py | 1 - .../plugins/publish/submit_nuke_deadline.py | 1 - .../publish/abstract_collect_render.py | 1 - .../plugins/publish/collect_farm_target.py | 45 +++++++++++++++++++ 11 files changed, 46 insertions(+), 10 deletions(-) create mode 100644 openpype/plugins/publish/collect_farm_target.py diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_render.py b/openpype/hosts/aftereffects/plugins/publish/collect_render.py index aa46461915..49874d6cff 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_render.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_render.py @@ -138,7 +138,6 @@ class CollectAERender(publish.AbstractCollectRender): fam = "render.farm" if fam not in instance.families: instance.families.append(fam) - instance.toBeRenderedOn = "deadline" instance.renderer = "aerender" instance.farm = True # to skip integrate if "review" in instance.families: diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py index a20a142701..341f3f191a 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_render.py +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -108,7 +108,6 @@ class CollectFusionRender( fam = "render.farm" if fam not in instance.families: instance.families.append(fam) - instance.toBeRenderedOn = "deadline" instance.farm = True # to skip integrate if "review" in instance.families: # to skip ExtractReview locally diff --git a/openpype/hosts/harmony/plugins/publish/collect_farm_render.py b/openpype/hosts/harmony/plugins/publish/collect_farm_render.py index 5e9b9094a7..5daa93cddb 100644 --- a/openpype/hosts/harmony/plugins/publish/collect_farm_render.py +++ b/openpype/hosts/harmony/plugins/publish/collect_farm_render.py @@ -174,7 +174,6 @@ class CollectFarmRender(publish.AbstractCollectRender): outputFormat=info[1], outputStartFrame=info[3], leadingZeros=info[2], - toBeRenderedOn='deadline', ignoreFrameHandleCheck=True ) diff --git a/openpype/hosts/maya/plugins/publish/submit_maya_muster.py b/openpype/hosts/maya/plugins/publish/submit_maya_muster.py index 8e219eae85..b79c9ed140 100644 --- a/openpype/hosts/maya/plugins/publish/submit_maya_muster.py +++ b/openpype/hosts/maya/plugins/publish/submit_maya_muster.py @@ -249,7 +249,6 @@ class MayaSubmitMuster(pyblish.api.InstancePlugin): Authenticate with Muster, collect all data, prepare path for post render publish job and submit job to farm. """ - instance.data["toBeRenderedOn"] = "muster" # setup muster environment self.MUSTER_REST_URL = os.environ.get("MUSTER_REST_URL") diff --git a/openpype/modules/deadline/plugins/publish/submit_celaction_deadline.py b/openpype/modules/deadline/plugins/publish/submit_celaction_deadline.py index ee28612b44..4aef914023 100644 --- a/openpype/modules/deadline/plugins/publish/submit_celaction_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_celaction_deadline.py @@ -27,7 +27,7 @@ class CelactionSubmitDeadline(pyblish.api.InstancePlugin): deadline_job_delay = "00:00:08:00" def process(self, instance): - instance.data["toBeRenderedOn"] = "deadline" + context = instance.context # get default deadline webservice url from deadline module diff --git a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py index 108c377078..8f21a920be 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -141,4 +141,3 @@ class HoudiniSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): # Store output dir for unified publisher (filesequence) output_dir = os.path.dirname(instance.data["files"][0]) instance.data["outputDir"] = output_dir - instance.data["toBeRenderedOn"] = "deadline" diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index 8e05582962..5d38903770 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -174,7 +174,6 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, first_file = next(self._iter_expected_files(files)) output_dir = os.path.dirname(first_file) instance.data["outputDir"] = output_dir - instance.data["toBeRenderedOn"] = "deadline" filename = os.path.basename(filepath) diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py index 75d24b28f0..34f3905a17 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -300,7 +300,6 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, first_file = next(iter_expected_files(expected_files)) output_dir = os.path.dirname(first_file) instance.data["outputDir"] = output_dir - instance.data["toBeRenderedOn"] = "deadline" # Patch workfile (only when use_published is enabled) if self.use_published: diff --git a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py index cfdeb4968b..ded5cd179f 100644 --- a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py @@ -97,7 +97,6 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin, instance.data["suspend_publish"] = instance.data["attributeValues"][ "suspend_publish"] - instance.data["toBeRenderedOn"] = "deadline" families = instance.data["families"] node = instance.data["transientData"]["node"] diff --git a/openpype/pipeline/publish/abstract_collect_render.py b/openpype/pipeline/publish/abstract_collect_render.py index 6877d556c3..8a26402bd8 100644 --- a/openpype/pipeline/publish/abstract_collect_render.py +++ b/openpype/pipeline/publish/abstract_collect_render.py @@ -75,7 +75,6 @@ class RenderInstance(object): tilesY = attr.ib(default=0) # number of tiles in Y # submit_publish_job - toBeRenderedOn = attr.ib(default=None) deadlineSubmissionJob = attr.ib(default=None) anatomyData = attr.ib(default=None) outputDir = attr.ib(default=None) diff --git a/openpype/plugins/publish/collect_farm_target.py b/openpype/plugins/publish/collect_farm_target.py new file mode 100644 index 0000000000..78410835dd --- /dev/null +++ b/openpype/plugins/publish/collect_farm_target.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +import pyblish.api + + +class CollectFarmTarget(pyblish.api.InstancePlugin): + """Collects the render target for the instance + """ + + order = pyblish.api.CollectorOrder + 0.499 + label = "Collect Farm Target" + targets = ["local"] + + def process(self, instance): + if not instance.data.get("farm"): + return + + context = instance.context + try: + deadline_module = context.data.get("openPypeModules")["deadline"] + if deadline_module.enabled: + instance.data["toBeRenderedOn"] = "deadline" + self.log.debug("Collected render target: deadline") + except AttributeError: + self.log.error("Cannot get OpenPype Deadline module.") + raise AssertionError("OpenPype Deadline module not found.") + + try: + royalrender_module = \ + context.data.get("openPypeModules")["royalrender"] + if royalrender_module.enabled: + instance.data["toBeRenderedOn"] = "royalrender" + self.log.debug("Collected render target: royalrender") + + except AttributeError: + self.log.error("Cannot get OpenPype RoyalRender module.") + raise AssertionError("OpenPype RoyalRender module not found.") + + try: + muster_module = context.data.get("openPypeModules")["muster"] + if muster_module.enabled: + instance.data["toBeRenderedOn"] = "muster" + self.log.debug("Collected render target: muster") + except AttributeError: + self.log.error("Cannot get OpenPype Muster module.") + raise AssertionError("OpenPype Muster module not found.") From 098bacddb9e87ac5eb7c8b9e5f1ef2d3f43fa74f Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 25 Aug 2023 11:54:44 +0800 Subject: [PATCH 159/327] fix incorrect position of the container during updating --- openpype/hosts/max/api/lib.py | 32 +++++++++++++++++++ .../hosts/max/plugins/load/load_camera_fbx.py | 11 ++++++- .../hosts/max/plugins/load/load_max_scene.py | 12 +++++-- .../hosts/max/plugins/load/load_model_fbx.py | 11 ++++++- .../hosts/max/plugins/load/load_model_obj.py | 12 ++++++- .../hosts/max/plugins/load/load_model_usd.py | 14 ++++++-- 6 files changed, 85 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 08819ba155..267e75e5fe 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -371,9 +371,41 @@ def unique_namespace(namespace, format="%02d", def get_namespace(container_name): + """Get the namespace and name of the sub-container + + Args: + container_name (str): the name of master container + + Raises: + RuntimeError: when there is no master container found + + Returns: + namespace (str): namespace of the sub-container + name (str): name of the sub-container + """ node = rt.getNodeByName(container_name) if not node: raise RuntimeError("Master Container Not Found..") name = rt.getUserProp(node, "name") namespace = rt.getUserProp(node, "namespace") return namespace, name + +def object_transform_set(container_children): + """A function which allows to store the transform of + previous loaded object(s) + Args: + container_children(list): A list of nodes + + Returns: + transform_set (dict): A dict with all transform data of + the previous loaded object(s) + """ + transform_set = {} + for node in container_children: + name = f"{node.name}.transform" + transform_set[name] = node.pos + name = f"{node.name}.scale" + transform_set[name] = node.scale + name = f"{node.name}.rotation" + transform_set[name] = node.rotation + return transform_set diff --git a/openpype/hosts/max/plugins/load/load_camera_fbx.py b/openpype/hosts/max/plugins/load/load_camera_fbx.py index c70ece6293..acd77ad686 100644 --- a/openpype/hosts/max/plugins/load/load_camera_fbx.py +++ b/openpype/hosts/max/plugins/load/load_camera_fbx.py @@ -2,7 +2,9 @@ import os from openpype.hosts.max.api import lib, maintained_selection from openpype.hosts.max.api.lib import ( - unique_namespace, get_namespace + unique_namespace, + get_namespace, + object_transform_set ) from openpype.hosts.max.api.pipeline import ( containerise, @@ -61,6 +63,7 @@ class FbxLoader(load.LoaderPlugin): sub_node_name = f"{namespace}:{name}" inst_container = rt.getNodeByName(sub_node_name) rt.Select(inst_container.Children) + transform_data = object_transform_set(inst_container.Children) for prev_fbx_obj in rt.selection: if rt.isValidNode(prev_fbx_obj): rt.Delete(prev_fbx_obj) @@ -77,6 +80,12 @@ class FbxLoader(load.LoaderPlugin): if fbx_object.Parent != inst_container: fbx_object.Parent = inst_container fbx_object.name = f"{namespace}:{fbx_object.name}" + fbx_object.pos = transform_data[ + f"{fbx_object.name}.transform"] + fbx_object.rotation = transform_data[ + f"{fbx_object.name}.rotation"] + fbx_object.scale = transform_data[ + f"{fbx_object.name}.scale"] for children in node.Children: if rt.classOf(children) == rt.Container: diff --git a/openpype/hosts/max/plugins/load/load_max_scene.py b/openpype/hosts/max/plugins/load/load_max_scene.py index fada871c6d..3d524e261f 100644 --- a/openpype/hosts/max/plugins/load/load_max_scene.py +++ b/openpype/hosts/max/plugins/load/load_max_scene.py @@ -2,7 +2,9 @@ import os from openpype.hosts.max.api import lib from openpype.hosts.max.api.lib import ( - unique_namespace, get_namespace + unique_namespace, + get_namespace, + object_transform_set ) from openpype.hosts.max.api.pipeline import ( containerise, import_custom_attribute_data, @@ -62,6 +64,7 @@ class MaxSceneLoader(load.LoaderPlugin): # delete the old container with attribute # delete old duplicate rt.Select(node.Children) + transform_data = object_transform_set(node.Children) for prev_max_obj in rt.GetCurrentSelection(): if rt.isValidNode(prev_max_obj) and prev_max_obj.name != sub_container_name: # noqa rt.Delete(prev_max_obj) @@ -77,7 +80,12 @@ class MaxSceneLoader(load.LoaderPlugin): for max_obj, obj_name in zip(current_max_objects, current_max_object_names): max_obj.name = f"{namespace}:{obj_name}" - + max_obj.pos = transform_data[ + f"{max_obj.name}.transform"] + max_obj.rotation = transform_data[ + f"{max_obj.name}.rotation"] + max_obj.scale = transform_data[ + f"{max_obj.name}.scale"] lib.imprint(container["instance_node"], { "representation": str(representation["_id"]) diff --git a/openpype/hosts/max/plugins/load/load_model_fbx.py b/openpype/hosts/max/plugins/load/load_model_fbx.py index 6097a4ca6e..fcac72dae1 100644 --- a/openpype/hosts/max/plugins/load/load_model_fbx.py +++ b/openpype/hosts/max/plugins/load/load_model_fbx.py @@ -6,7 +6,9 @@ from openpype.hosts.max.api.pipeline import ( ) from openpype.hosts.max.api import lib from openpype.hosts.max.api.lib import ( - unique_namespace, get_namespace + unique_namespace, + get_namespace, + object_transform_set ) from openpype.hosts.max.api.lib import maintained_selection @@ -56,6 +58,7 @@ class FbxModelLoader(load.LoaderPlugin): sub_node_name = f"{namespace}:{name}" inst_container = rt.getNodeByName(sub_node_name) rt.Select(inst_container.Children) + transform_data = object_transform_set(inst_container.Children) for prev_fbx_obj in rt.selection: if rt.isValidNode(prev_fbx_obj): rt.Delete(prev_fbx_obj) @@ -71,6 +74,12 @@ class FbxModelLoader(load.LoaderPlugin): if fbx_object.Parent != inst_container: fbx_object.Parent = inst_container fbx_object.name = f"{namespace}:{fbx_object.name}" + fbx_object.pos = transform_data[ + f"{fbx_object.name}.transform"] + fbx_object.rotation = transform_data[ + f"{fbx_object.name}.rotation"] + fbx_object.scale = transform_data[ + f"{fbx_object.name}.scale"] for children in node.Children: if rt.classOf(children) == rt.Container: diff --git a/openpype/hosts/max/plugins/load/load_model_obj.py b/openpype/hosts/max/plugins/load/load_model_obj.py index 225801b8d0..04a0ac1679 100644 --- a/openpype/hosts/max/plugins/load/load_model_obj.py +++ b/openpype/hosts/max/plugins/load/load_model_obj.py @@ -2,7 +2,10 @@ import os from openpype.hosts.max.api import lib from openpype.hosts.max.api.lib import ( - unique_namespace, get_namespace + unique_namespace, + get_namespace, + maintained_selection, + object_transform_set ) from openpype.hosts.max.api.lib import maintained_selection from openpype.hosts.max.api.pipeline import ( @@ -56,6 +59,7 @@ class ObjLoader(load.LoaderPlugin): sub_node_name = f"{namespace}:{name}" inst_container = rt.getNodeByName(sub_node_name) rt.Select(inst_container.Children) + transform_data = object_transform_set(inst_container.Children) for prev_obj in rt.selection: if rt.isValidNode(prev_obj): rt.Delete(prev_obj) @@ -67,6 +71,12 @@ class ObjLoader(load.LoaderPlugin): for selection in selections: selection.Parent = inst_container selection.name = f"{namespace}:{selection.name}" + selection.pos = transform_data[ + f"{selection.name}.transform"] + selection.rotation = transform_data[ + f"{selection.name}.rotation"] + selection.scale = transform_data[ + f"{selection.name}.scale"] with maintained_selection(): rt.Select(node) diff --git a/openpype/hosts/max/plugins/load/load_model_usd.py b/openpype/hosts/max/plugins/load/load_model_usd.py index 0c17736739..14f339f039 100644 --- a/openpype/hosts/max/plugins/load/load_model_usd.py +++ b/openpype/hosts/max/plugins/load/load_model_usd.py @@ -2,7 +2,9 @@ import os from openpype.hosts.max.api import lib from openpype.hosts.max.api.lib import ( - unique_namespace, get_namespace + unique_namespace, + get_namespace, + object_transform_set ) from openpype.hosts.max.api.lib import maintained_selection from openpype.hosts.max.api.pipeline import ( @@ -63,8 +65,10 @@ class ModelUSDLoader(load.LoaderPlugin): node = rt.GetNodeByName(node_name) namespace, name = get_namespace(node_name) sub_node_name = f"{namespace}:{name}" + transform_data = None for n in node.Children: rt.Select(n.Children) + transform_data = object_transform_set(n.Children) for prev_usd_asset in rt.selection: if rt.isValidNode(prev_usd_asset): rt.Delete(prev_usd_asset) @@ -85,8 +89,14 @@ class ModelUSDLoader(load.LoaderPlugin): import_custom_attribute_data(asset, asset.Children) for children in asset.Children: children.name = f"{namespace}:{children.name}" - asset.name = sub_node_name + children.pos = transform_data[ + f"{children.name}.transform"] + children.rotation = transform_data[ + f"{children.name}.rotation"] + children.scale = transform_data[ + f"{children.name}.scale"] + asset.name = sub_node_name with maintained_selection(): rt.Select(node) From b490b2741f75d51d1dc4f6ec6fbbab238d11fa0c Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 25 Aug 2023 11:56:01 +0800 Subject: [PATCH 160/327] hound --- openpype/hosts/max/api/lib.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 267e75e5fe..712340c99a 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -390,6 +390,7 @@ def get_namespace(container_name): namespace = rt.getUserProp(node, "namespace") return namespace, name + def object_transform_set(container_children): """A function which allows to store the transform of previous loaded object(s) From e9952b5d53421794b58b278d015412f368baf88e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 25 Aug 2023 11:26:03 +0200 Subject: [PATCH 161/327] global: avoiding cleanup of flagged representation --- openpype/pipeline/publish/lib.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index ada12800a9..08ce8c2c9d 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -953,6 +953,7 @@ def replace_with_published_scene_path(instance, replace_in_path=True): return file_path + def add_repre_files_for_cleanup(instance, repre): """ Explicitly mark repre files to be deleted. @@ -961,7 +962,16 @@ def add_repre_files_for_cleanup(instance, repre): """ files = repre["files"] staging_dir = repre.get("stagingDir") - if not staging_dir or instance.data.get("stagingDir_persistent"): + + # first make sure representation level is not persistent + if ( + not staging_dir + or repre.get("stagingDir_persistent") + ): + return + + # then look into instance level if it's not persistent + if instance.data.get("stagingDir_persistent"): return if isinstance(files, str): From 499b4623a30a0b4d7d900683d119394f27304540 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 24 Aug 2023 15:41:21 +0200 Subject: [PATCH 162/327] adding abstraction of publishing related functions --- openpype/pipeline/colorspace.py | 153 ++++++++++++++++++++++++++++++-- 1 file changed, 144 insertions(+), 9 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 731132911a..649d355f62 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -13,12 +13,17 @@ from openpype.lib import ( Logger ) from openpype.pipeline import Anatomy +from openpype.lib.transcoding import VIDEO_EXTENSIONS, IMAGE_EXTENSIONS + log = Logger.get_logger(__name__) -class CashedData: +class CachedData: remapping = None + allowed_exts = { + ext.lstrip(".") for ext in IMAGE_EXTENSIONS.union(VIDEO_EXTENSIONS) + } @contextlib.contextmanager @@ -546,15 +551,15 @@ def get_remapped_colorspace_to_native( Union[str, None]: native colorspace name defined in remapping or None """ - CashedData.remapping.setdefault(host_name, {}) - if CashedData.remapping[host_name].get("to_native") is None: + CachedData.remapping.setdefault(host_name, {}) + if CachedData.remapping[host_name].get("to_native") is None: remapping_rules = imageio_host_settings["remapping"]["rules"] - CashedData.remapping[host_name]["to_native"] = { + CachedData.remapping[host_name]["to_native"] = { rule["ocio_name"]: rule["host_native_name"] for rule in remapping_rules } - return CashedData.remapping[host_name]["to_native"].get( + return CachedData.remapping[host_name]["to_native"].get( ocio_colorspace_name) @@ -572,15 +577,15 @@ def get_remapped_colorspace_from_native( Union[str, None]: Ocio colorspace name defined in remapping or None. """ - CashedData.remapping.setdefault(host_name, {}) - if CashedData.remapping[host_name].get("from_native") is None: + CachedData.remapping.setdefault(host_name, {}) + if CachedData.remapping[host_name].get("from_native") is None: remapping_rules = imageio_host_settings["remapping"]["rules"] - CashedData.remapping[host_name]["from_native"] = { + CachedData.remapping[host_name]["from_native"] = { rule["host_native_name"]: rule["ocio_name"] for rule in remapping_rules } - return CashedData.remapping[host_name]["from_native"].get( + return CachedData.remapping[host_name]["from_native"].get( host_native_colorspace_name) @@ -601,3 +606,133 @@ def _get_imageio_settings(project_settings, host_name): imageio_host = project_settings.get(host_name, {}).get("imageio", {}) return imageio_global, imageio_host + + +def get_colorspace_settings_from_publish_context(context_data): + """Returns solved settings for the host context. + + Args: + context_data (publish.Context.data): publishing context data + + Returns: + tuple | bool: config, file rules or None + """ + if "imageioSettings" in context_data: + return context_data["imageioSettings"] + + project_name = context_data["projectName"] + host_name = context_data["hostName"] + anatomy_data = context_data["anatomyData"] + project_settings_ = context_data["project_settings"] + + config_data = get_imageio_config( + project_name, host_name, + project_settings=project_settings_, + anatomy_data=anatomy_data + ) + + # in case host color management is not enabled + if not config_data: + return None + + file_rules = get_imageio_file_rules( + project_name, host_name, + project_settings=project_settings_ + ) + + # caching settings for future instance processing + context_data["imageioSettings"] = (config_data, file_rules) + + return config_data, file_rules + + +def set_colorspace_data_to_representation( + representation, context_data, + colorspace=None, + colorspace_settings=None, + log=None +): + """Sets colorspace data to representation. + + Args: + representation (dict): publishing representation + context_data (publish.Context.data): publishing context data + config_data (dict): host resolved config data + file_rules (dict): host resolved file rules data + colorspace (str, optional): colorspace name. Defaults to None. + colorspace_settings (tuple[dict, dict], optional): + Settings for config_data and file_rules. + Defaults to None. + log (logging.Logger, optional): logger instance. Defaults to None. + + Example: + ``` + { + # for other publish plugins and loaders + "colorspace": "linear", + "config": { + # for future references in case need + "path": "/abs/path/to/config.ocio", + # for other plugins within remote publish cases + "template": "{project[root]}/path/to/config.ocio" + } + } + ``` + + """ + log = log or Logger.get_logger(__name__) + + file_ext = representation["ext"] + + # check if `file_ext` in lower case is in CachedData.allowed_exts + if file_ext.lstrip(".").lower() not in CachedData.allowed_exts: + log.debug( + "Extension '{}' is not in allowed extensions.".format(file_ext) + ) + return + + if colorspace_settings is None: + colorspace_settings = get_colorspace_settings_from_publish_context( + context_data) + + # in case host color management is not enabled + if not colorspace_settings: + log.warning("Host's colorspace management is disabled.") + return + + # unpack colorspace settings + config_data, file_rules = colorspace_settings + + if not config_data: + # warn in case no colorspace path was defined + log.warning("No colorspace management was defined") + return + + log.debug("Config data is: `{}`".format(config_data)) + + project_name = context_data["projectName"] + host_name = context_data["hostName"] + project_settings = context_data["project_settings"] + + # get one filename + filename = representation["files"] + if isinstance(filename, list): + filename = filename[0] + + # get matching colorspace from rules + colorspace = colorspace or get_imageio_colorspace_from_filepath( + filename, host_name, project_name, + config_data=config_data, + file_rules=file_rules, + project_settings=project_settings + ) + + # infuse data to representation + if colorspace: + colorspace_data = { + "colorspace": colorspace, + "config": config_data + } + + # update data key + representation["colorspaceData"] = colorspace_data From 8260eb36bdd3e1d4cb8590f59853099b11dd75a8 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 24 Aug 2023 16:39:12 +0200 Subject: [PATCH 163/327] implementing abstarctions from colorspace --- openpype/pipeline/publish/publish_plugins.py | 106 ++----------------- 1 file changed, 10 insertions(+), 96 deletions(-) diff --git a/openpype/pipeline/publish/publish_plugins.py b/openpype/pipeline/publish/publish_plugins.py index ba3be6397e..17ede069cb 100644 --- a/openpype/pipeline/publish/publish_plugins.py +++ b/openpype/pipeline/publish/publish_plugins.py @@ -1,6 +1,5 @@ import inspect from abc import ABCMeta -from pprint import pformat import pyblish.api from pyblish.plugin import MetaPlugin, ExplicitMetaPlugin from openpype.lib.transcoding import VIDEO_EXTENSIONS, IMAGE_EXTENSIONS @@ -14,9 +13,8 @@ from .lib import ( ) from openpype.pipeline.colorspace import ( - get_imageio_colorspace_from_filepath, - get_imageio_config, - get_imageio_file_rules + get_colorspace_settings_from_publish_context, + set_colorspace_data_to_representation ) @@ -306,12 +304,8 @@ class ColormanagedPyblishPluginMixin(object): matching colorspace from rules. Finally, it infuses this data into the representation. """ - allowed_ext = set( - ext.lstrip(".") for ext in IMAGE_EXTENSIONS.union(VIDEO_EXTENSIONS) - ) - @staticmethod - def get_colorspace_settings(context): + def get_colorspace_settings(self, context): """Returns solved settings for the host context. Args: @@ -320,33 +314,7 @@ class ColormanagedPyblishPluginMixin(object): Returns: tuple | bool: config, file rules or None """ - if "imageioSettings" in context.data: - return context.data["imageioSettings"] - - project_name = context.data["projectName"] - host_name = context.data["hostName"] - anatomy_data = context.data["anatomyData"] - project_settings_ = context.data["project_settings"] - - config_data = get_imageio_config( - project_name, host_name, - project_settings=project_settings_, - anatomy_data=anatomy_data - ) - - # in case host color management is not enabled - if not config_data: - return None - - file_rules = get_imageio_file_rules( - project_name, host_name, - project_settings=project_settings_ - ) - - # caching settings for future instance processing - context.data["imageioSettings"] = (config_data, file_rules) - - return config_data, file_rules + return get_colorspace_settings_from_publish_context(context.data) def set_representation_colorspace( self, representation, context, @@ -380,64 +348,10 @@ class ColormanagedPyblishPluginMixin(object): ``` """ - ext = representation["ext"] - # check extension - self.log.debug("__ ext: `{}`".format(ext)) - - # check if ext in lower case is in self.allowed_ext - if ext.lstrip(".").lower() not in self.allowed_ext: - self.log.debug( - "Extension '{}' is not in allowed extensions.".format(ext) - ) - return - - if colorspace_settings is None: - colorspace_settings = self.get_colorspace_settings(context) - - # in case host color management is not enabled - if not colorspace_settings: - self.log.warning("Host's colorspace management is disabled.") - return - - # unpack colorspace settings - config_data, file_rules = colorspace_settings - - if not config_data: - # warn in case no colorspace path was defined - self.log.warning("No colorspace management was defined") - return - - self.log.debug("Config data is: `{}`".format(config_data)) - - project_name = context.data["projectName"] - host_name = context.data["hostName"] - project_settings = context.data["project_settings"] - - # get one filename - filename = representation["files"] - if isinstance(filename, list): - filename = filename[0] - - self.log.debug("__ filename: `{}`".format(filename)) - - # get matching colorspace from rules - colorspace = colorspace or get_imageio_colorspace_from_filepath( - filename, host_name, project_name, - config_data=config_data, - file_rules=file_rules, - project_settings=project_settings + # using cached settings if available + set_colorspace_data_to_representation( + representation, context.data, + colorspace, + colorspace_settings, + log=self.log ) - self.log.debug("__ colorspace: `{}`".format(colorspace)) - - # infuse data to representation - if colorspace: - colorspace_data = { - "colorspace": colorspace, - "config": config_data - } - - # update data key - representation["colorspaceData"] = colorspace_data - - self.log.debug("__ colorspace_data: `{}`".format( - pformat(colorspace_data))) From 1a61eb0c3e711cfcf856f7f1ef937949161e89f4 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 25 Aug 2023 21:29:34 +0800 Subject: [PATCH 164/327] Libor's comment on Container namespace issue and not support rotation --- openpype/hosts/max/api/lib.py | 2 -- openpype/hosts/max/plugins/load/load_camera_fbx.py | 8 ++++---- openpype/hosts/max/plugins/load/load_max_scene.py | 7 +++---- openpype/hosts/max/plugins/load/load_model.py | 3 ++- openpype/hosts/max/plugins/load/load_model_fbx.py | 8 ++++---- openpype/hosts/max/plugins/load/load_model_obj.py | 7 +++---- openpype/hosts/max/plugins/load/load_model_usd.py | 7 +++---- openpype/hosts/max/plugins/load/load_pointcache.py | 3 ++- openpype/hosts/max/plugins/load/load_pointcloud.py | 6 ++++-- openpype/hosts/max/plugins/load/load_redshift_proxy.py | 6 ++++-- 10 files changed, 29 insertions(+), 28 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 712340c99a..034307e72a 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -407,6 +407,4 @@ def object_transform_set(container_children): transform_set[name] = node.pos name = f"{node.name}.scale" transform_set[name] = node.scale - name = f"{node.name}.rotation" - transform_set[name] = node.rotation return transform_set diff --git a/openpype/hosts/max/plugins/load/load_camera_fbx.py b/openpype/hosts/max/plugins/load/load_camera_fbx.py index acd77ad686..f040115417 100644 --- a/openpype/hosts/max/plugins/load/load_camera_fbx.py +++ b/openpype/hosts/max/plugins/load/load_camera_fbx.py @@ -22,6 +22,7 @@ class FbxLoader(load.LoaderPlugin): order = -9 icon = "code-fork" color = "white" + postfix = "param" def load(self, context, name=None, namespace=None, data=None): from pymxs import runtime as rt @@ -41,7 +42,8 @@ class FbxLoader(load.LoaderPlugin): name + "_", suffix="_", ) - container = rt.container(name=f"{namespace}:{name}") + container = rt.container( + name=f"{namespace}:{name}_{self.postfix}") selections = rt.GetCurrentSelection() import_custom_attribute_data(container, selections) @@ -60,7 +62,7 @@ class FbxLoader(load.LoaderPlugin): node_name = container["instance_node"] node = rt.getNodeByName(node_name) namespace, name = get_namespace(node_name) - sub_node_name = f"{namespace}:{name}" + sub_node_name = f"{namespace}:{name}_{self.postfix}" inst_container = rt.getNodeByName(sub_node_name) rt.Select(inst_container.Children) transform_data = object_transform_set(inst_container.Children) @@ -82,8 +84,6 @@ class FbxLoader(load.LoaderPlugin): fbx_object.name = f"{namespace}:{fbx_object.name}" fbx_object.pos = transform_data[ f"{fbx_object.name}.transform"] - fbx_object.rotation = transform_data[ - f"{fbx_object.name}.rotation"] fbx_object.scale = transform_data[ f"{fbx_object.name}.scale"] diff --git a/openpype/hosts/max/plugins/load/load_max_scene.py b/openpype/hosts/max/plugins/load/load_max_scene.py index 3d524e261f..98e9be96e1 100644 --- a/openpype/hosts/max/plugins/load/load_max_scene.py +++ b/openpype/hosts/max/plugins/load/load_max_scene.py @@ -24,6 +24,7 @@ class MaxSceneLoader(load.LoaderPlugin): order = -8 icon = "code-fork" color = "green" + postfix = "param" def load(self, context, name=None, namespace=None, data=None): from pymxs import runtime as rt @@ -41,7 +42,7 @@ class MaxSceneLoader(load.LoaderPlugin): name + "_", suffix="_", ) - container_name = f"{namespace}:{name}" + container_name = f"{namespace}:{name}_{self.postfix}" container = rt.Container(name=container_name) import_custom_attribute_data(container, max_objects) max_container.append(container) @@ -60,7 +61,7 @@ class MaxSceneLoader(load.LoaderPlugin): node = rt.getNodeByName(node_name) namespace, name = get_namespace(node_name) - sub_container_name = f"{namespace}:{name}" + sub_container_name = f"{namespace}:{name}_{self.postfix}" # delete the old container with attribute # delete old duplicate rt.Select(node.Children) @@ -82,8 +83,6 @@ class MaxSceneLoader(load.LoaderPlugin): max_obj.name = f"{namespace}:{obj_name}" max_obj.pos = transform_data[ f"{max_obj.name}.transform"] - max_obj.rotation = transform_data[ - f"{max_obj.name}.rotation"] max_obj.scale = transform_data[ f"{max_obj.name}.scale"] diff --git a/openpype/hosts/max/plugins/load/load_model.py b/openpype/hosts/max/plugins/load/load_model.py index acc2a4032b..c5a73b4327 100644 --- a/openpype/hosts/max/plugins/load/load_model.py +++ b/openpype/hosts/max/plugins/load/load_model.py @@ -20,6 +20,7 @@ class ModelAbcLoader(load.LoaderPlugin): order = -10 icon = "code-fork" color = "orange" + postfix = "param" def load(self, context, name=None, namespace=None, data=None): from pymxs import runtime as rt @@ -61,7 +62,7 @@ class ModelAbcLoader(load.LoaderPlugin): for abc_object in abc_container.Children: abc_object.name = f"{namespace}:{abc_object.name}" # rename the abc container with namespace - abc_container_name = f"{namespace}:{name}" + abc_container_name = f"{namespace}:{name}_{self.postfix}" abc_container.name = abc_container_name return containerise( diff --git a/openpype/hosts/max/plugins/load/load_model_fbx.py b/openpype/hosts/max/plugins/load/load_model_fbx.py index fcac72dae1..56c8768675 100644 --- a/openpype/hosts/max/plugins/load/load_model_fbx.py +++ b/openpype/hosts/max/plugins/load/load_model_fbx.py @@ -21,6 +21,7 @@ class FbxModelLoader(load.LoaderPlugin): order = -9 icon = "code-fork" color = "white" + postfix = "param" def load(self, context, name=None, namespace=None, data=None): from pymxs import runtime as rt @@ -36,7 +37,8 @@ class FbxModelLoader(load.LoaderPlugin): name + "_", suffix="_", ) - container = rt.container(name=f"{namespace}:{name}") + container = rt.container( + name=f"{namespace}:{name}_{self.postfix}") selections = rt.GetCurrentSelection() import_custom_attribute_data(container, selections) @@ -55,7 +57,7 @@ class FbxModelLoader(load.LoaderPlugin): node_name = container["instance_node"] node = rt.getNodeByName(node_name) namespace, name = get_namespace(node_name) - sub_node_name = f"{namespace}:{name}" + sub_node_name = f"{namespace}:{name}_{self.postfix}" inst_container = rt.getNodeByName(sub_node_name) rt.Select(inst_container.Children) transform_data = object_transform_set(inst_container.Children) @@ -76,8 +78,6 @@ class FbxModelLoader(load.LoaderPlugin): fbx_object.name = f"{namespace}:{fbx_object.name}" fbx_object.pos = transform_data[ f"{fbx_object.name}.transform"] - fbx_object.rotation = transform_data[ - f"{fbx_object.name}.rotation"] fbx_object.scale = transform_data[ f"{fbx_object.name}.scale"] diff --git a/openpype/hosts/max/plugins/load/load_model_obj.py b/openpype/hosts/max/plugins/load/load_model_obj.py index 04a0ac1679..314889e6ec 100644 --- a/openpype/hosts/max/plugins/load/load_model_obj.py +++ b/openpype/hosts/max/plugins/load/load_model_obj.py @@ -24,6 +24,7 @@ class ObjLoader(load.LoaderPlugin): order = -9 icon = "code-fork" color = "white" + postfix = "param" def load(self, context, name=None, namespace=None, data=None): from pymxs import runtime as rt @@ -38,7 +39,7 @@ class ObjLoader(load.LoaderPlugin): suffix="_", ) # create "missing" container for obj import - container = rt.Container(name=f"{namespace}:{name}") + container = rt.Container(name=f"{namespace}:{name}_{self.postfix}") selections = rt.GetCurrentSelection() import_custom_attribute_data(container, selections) # get current selection @@ -56,7 +57,7 @@ class ObjLoader(load.LoaderPlugin): node_name = container["instance_node"] node = rt.getNodeByName(node_name) namespace, name = get_namespace(node_name) - sub_node_name = f"{namespace}:{name}" + sub_node_name = f"{namespace}:{name}_{self.postfix}" inst_container = rt.getNodeByName(sub_node_name) rt.Select(inst_container.Children) transform_data = object_transform_set(inst_container.Children) @@ -73,8 +74,6 @@ class ObjLoader(load.LoaderPlugin): selection.name = f"{namespace}:{selection.name}" selection.pos = transform_data[ f"{selection.name}.transform"] - selection.rotation = transform_data[ - f"{selection.name}.rotation"] selection.scale = transform_data[ f"{selection.name}.scale"] with maintained_selection(): diff --git a/openpype/hosts/max/plugins/load/load_model_usd.py b/openpype/hosts/max/plugins/load/load_model_usd.py index 14f339f039..f35d8e6327 100644 --- a/openpype/hosts/max/plugins/load/load_model_usd.py +++ b/openpype/hosts/max/plugins/load/load_model_usd.py @@ -23,6 +23,7 @@ class ModelUSDLoader(load.LoaderPlugin): order = -10 icon = "code-fork" color = "orange" + postfix = "param" def load(self, context, name=None, namespace=None, data=None): from pymxs import runtime as rt @@ -47,7 +48,7 @@ class ModelUSDLoader(load.LoaderPlugin): for usd_asset in asset.Children: usd_asset.name = f"{namespace}:{usd_asset.name}" - asset_name = f"{namespace}:{name}" + asset_name = f"{namespace}:{name}_{self.postfix}" asset.name = asset_name # need to get the correct container after renamed asset = rt.GetNodeByName(asset_name) @@ -64,7 +65,7 @@ class ModelUSDLoader(load.LoaderPlugin): node_name = container["instance_node"] node = rt.GetNodeByName(node_name) namespace, name = get_namespace(node_name) - sub_node_name = f"{namespace}:{name}" + sub_node_name = f"{namespace}:{name}_{self.postfix}" transform_data = None for n in node.Children: rt.Select(n.Children) @@ -91,8 +92,6 @@ class ModelUSDLoader(load.LoaderPlugin): children.name = f"{namespace}:{children.name}" children.pos = transform_data[ f"{children.name}.transform"] - children.rotation = transform_data[ - f"{children.name}.rotation"] children.scale = transform_data[ f"{children.name}.scale"] diff --git a/openpype/hosts/max/plugins/load/load_pointcache.py b/openpype/hosts/max/plugins/load/load_pointcache.py index 64bf7ddac0..070dea88d4 100644 --- a/openpype/hosts/max/plugins/load/load_pointcache.py +++ b/openpype/hosts/max/plugins/load/load_pointcache.py @@ -24,6 +24,7 @@ class AbcLoader(load.LoaderPlugin): order = -10 icon = "code-fork" color = "orange" + postfix = "param" def load(self, context, name=None, namespace=None, data=None): from pymxs import runtime as rt @@ -68,7 +69,7 @@ class AbcLoader(load.LoaderPlugin): for abc_object in abc_container.Children: abc_object.name = f"{namespace}:{abc_object.name}" # rename the abc container with namespace - abc_container_name = f"{namespace}:{name}" + abc_container_name = f"{namespace}:{name}_{self.postfix}" abc_container.name = abc_container_name return containerise( diff --git a/openpype/hosts/max/plugins/load/load_pointcloud.py b/openpype/hosts/max/plugins/load/load_pointcloud.py index 87b7fce292..c4c4cfbc6c 100644 --- a/openpype/hosts/max/plugins/load/load_pointcloud.py +++ b/openpype/hosts/max/plugins/load/load_pointcloud.py @@ -20,6 +20,7 @@ class PointCloudLoader(load.LoaderPlugin): order = -8 icon = "code-fork" color = "green" + postfix = "param" def load(self, context, name=None, namespace=None, data=None): """load point cloud by tyCache""" @@ -33,7 +34,8 @@ class PointCloudLoader(load.LoaderPlugin): name + "_", suffix="_", ) - prt_container = rt.Container(name=f"{namespace}:{name}") + prt_container = rt.Container( + name=f"{namespace}:{name}_{self.postfix}") import_custom_attribute_data(prt_container, [obj]) obj.Parent = prt_container obj.name = f"{namespace}:{obj.name}" @@ -49,7 +51,7 @@ class PointCloudLoader(load.LoaderPlugin): path = get_representation_path(representation) node = rt.GetNodeByName(container["instance_node"]) namespace, name = get_namespace(container["instance_node"]) - sub_node_name = f"{namespace}:{name}" + sub_node_name = f"{namespace}:{name}_{self.postfix}" inst_container = rt.getNodeByName(sub_node_name) update_custom_attribute_data( inst_container, inst_container.Children) diff --git a/openpype/hosts/max/plugins/load/load_redshift_proxy.py b/openpype/hosts/max/plugins/load/load_redshift_proxy.py index b240714314..f7dd95962b 100644 --- a/openpype/hosts/max/plugins/load/load_redshift_proxy.py +++ b/openpype/hosts/max/plugins/load/load_redshift_proxy.py @@ -25,6 +25,7 @@ class RedshiftProxyLoader(load.LoaderPlugin): order = -9 icon = "code-fork" color = "white" + postfix = "param" def load(self, context, name=None, namespace=None, data=None): from pymxs import runtime as rt @@ -41,7 +42,8 @@ class RedshiftProxyLoader(load.LoaderPlugin): name + "_", suffix="_", ) - container = rt.Container(name=f"{namespace}:{name}") + container = rt.Container( + name=f"{namespace}:{name}_{self.postfix}") rs_proxy.Parent = container rs_proxy.name = f"{namespace}:{rs_proxy.name}" import_custom_attribute_data(container, [rs_proxy]) @@ -55,7 +57,7 @@ class RedshiftProxyLoader(load.LoaderPlugin): path = get_representation_path(representation) namespace, name = get_namespace(container["instance_node"]) - sub_node_name = f"{namespace}:{name}" + sub_node_name = f"{namespace}:{name}_{self.postfix}" inst_container = rt.getNodeByName(sub_node_name) update_custom_attribute_data( From 2477287095d1d3e517d15c609a3796901043f0e2 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 25 Aug 2023 16:19:49 +0200 Subject: [PATCH 165/327] Publisher: Avoid warnings on thumbnails if source image also has alpha channel (#5510) * Avoid warnings if source image also has alpha channel * Fix logging typo --- openpype/plugins/publish/extract_thumbnail_from_source.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_thumbnail_from_source.py b/openpype/plugins/publish/extract_thumbnail_from_source.py index 54622bb84e..1b9f0a8bae 100644 --- a/openpype/plugins/publish/extract_thumbnail_from_source.py +++ b/openpype/plugins/publish/extract_thumbnail_from_source.py @@ -128,7 +128,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): if thumbnail_created: return full_output_path - self.log.warning("Thumbanil has not been created.") + self.log.warning("Thumbnail has not been created.") def _instance_has_thumbnail(self, instance): if "representations" not in instance.data: @@ -147,6 +147,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): oiio_cmd = get_oiio_tool_args( "oiiotool", "-a", src_path, + "--ch", "R,G,B", "-o", dst_path ) self.log.info("Running: {}".format(" ".join(oiio_cmd))) From 39f8e65177c9ea941a345e9c3bcafe9b2edfad0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Fri, 25 Aug 2023 16:20:20 +0200 Subject: [PATCH 166/327] if no Tag in representation avoiding raise (#5511) --- openpype/pipeline/farm/pyblish_functions.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/pipeline/farm/pyblish_functions.py b/openpype/pipeline/farm/pyblish_functions.py index 288602b77c..fe3ab97de8 100644 --- a/openpype/pipeline/farm/pyblish_functions.py +++ b/openpype/pipeline/farm/pyblish_functions.py @@ -139,7 +139,7 @@ def get_transferable_representations(instance): to_transfer = [] for representation in instance.data.get("representations", []): - if "publish_on_farm" not in representation.get("tags"): + if "publish_on_farm" not in representation.get("tags", []): continue trans_rep = representation.copy() @@ -265,8 +265,7 @@ def create_skeleton_instance( instance_skeleton_data[v] = instance.data.get(v) representations = get_transferable_representations(instance) - instance_skeleton_data["representations"] = [] - instance_skeleton_data["representations"] += representations + instance_skeleton_data["representations"] = representations persistent = instance.data.get("stagingDir_persistent") is True instance_skeleton_data["stagingDir_persistent"] = persistent From 5ed243e36ef098209c968f119366683d18c4fb5d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 25 Aug 2023 17:08:34 +0200 Subject: [PATCH 167/327] Publisher: Fix multiselection value (#5505) * fix change of value on multiselection * fix create - publish page change --- openpype/tools/attribute_defs/widgets.py | 8 ++++++++ openpype/tools/publisher/widgets/overview_widget.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/openpype/tools/attribute_defs/widgets.py b/openpype/tools/attribute_defs/widgets.py index d46c238da1..7967416e9f 100644 --- a/openpype/tools/attribute_defs/widgets.py +++ b/openpype/tools/attribute_defs/widgets.py @@ -343,6 +343,7 @@ class TextAttrWidget(_BaseAttrDefWidget): return self._input_widget.text() def set_value(self, value, multivalue=False): + block_signals = False if multivalue: set_value = set(value) if None in set_value: @@ -352,13 +353,18 @@ class TextAttrWidget(_BaseAttrDefWidget): if len(set_value) == 1: value = tuple(set_value)[0] else: + block_signals = True value = "< Multiselection >" if value != self.current_value(): + if block_signals: + self._input_widget.blockSignals(True) if self.multiline: self._input_widget.setPlainText(value) else: self._input_widget.setText(value) + if block_signals: + self._input_widget.blockSignals(False) class BoolAttrWidget(_BaseAttrDefWidget): @@ -391,7 +397,9 @@ class BoolAttrWidget(_BaseAttrDefWidget): set_value.add(self.attr_def.default) if len(set_value) > 1: + self._input_widget.blockSignals(True) self._input_widget.setCheckState(QtCore.Qt.PartiallyChecked) + self._input_widget.blockSignals(False) return value = tuple(set_value)[0] diff --git a/openpype/tools/publisher/widgets/overview_widget.py b/openpype/tools/publisher/widgets/overview_widget.py index 470645b9ee..778aa1139f 100644 --- a/openpype/tools/publisher/widgets/overview_widget.py +++ b/openpype/tools/publisher/widgets/overview_widget.py @@ -168,7 +168,7 @@ class OverviewWidget(QtWidgets.QFrame): def make_sure_animation_is_finished(self): if self._change_anim.state() == QtCore.QAbstractAnimation.Running: self._change_anim.stop() - self._on_change_anim_finished() + self._on_change_anim_finished() def set_state(self, new_state, animate): if new_state == self._current_state: From f38f1b451d6af4f65933a544a8a62b7998bc15d9 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 26 Aug 2023 03:25:15 +0000 Subject: [PATCH 168/327] [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 f8a49f8466..6d89e1eeae 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.16.5-nightly.1" +__version__ = "3.16.5-nightly.2" From 437f483bc48c8f8cdb38b181d9a1c6ec305388aa Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 26 Aug 2023 03:25:53 +0000 Subject: [PATCH 169/327] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 5c264e4d98..d7e49de5cb 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,8 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.16.5-nightly.2 + - 3.16.5-nightly.1 - 3.16.4 - 3.16.4-nightly.3 - 3.16.4-nightly.2 @@ -133,8 +135,6 @@ body: - 3.14.8-nightly.4 - 3.14.8-nightly.3 - 3.14.8-nightly.2 - - 3.14.8-nightly.1 - - 3.14.7 validations: required: true - type: dropdown From 3f1fb736a60d5ae88e4a425b2d2ce75f3554dae4 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 28 Aug 2023 18:45:43 +0800 Subject: [PATCH 170/327] make sure arnold and other renderers not fallbacking to workfile --- .../deadline/plugins/publish/submit_max_deadline.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index 8e05582962..f1127f60f2 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -12,7 +12,9 @@ from openpype.pipeline import ( legacy_io, OpenPypePyblishPluginMixin ) -from openpype.settings import get_project_settings +from openpype.pipeline.publish.lib import ( + replace_with_published_scene_path +) from openpype.hosts.max.api.lib import ( get_current_renderer, get_multipass_setting @@ -247,7 +249,12 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, if instance.data["renderer"] == "Redshift_Renderer": self.log.debug("Using Redshift...published scene wont be used..") replace_in_path = False - return replace_in_path + return replace_with_published_scene_path( + instance, replace_in_path) + else: + return replace_with_published_scene_path( + instance, replace_in_path) + @staticmethod def _iter_expected_files(exp): From 6e50d1a814c3765352038c5bf640bd9bc5409f43 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 28 Aug 2023 12:47:43 +0200 Subject: [PATCH 171/327] Fix log message - actually provide plugin name after "Plugin" --- openpype/pipeline/publish/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index ada12800a9..810e1dd342 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -465,7 +465,7 @@ def apply_plugin_settings_automatically(plugin, settings, logger=None): for option, value in settings.items(): if logger: logger.debug("Plugin {} - Attr: {} -> {}".format( - option, value, plugin.__name__ + plugin.__name__, option, value )) setattr(plugin, option, value) From a59de7db311bfeca3114c51920eed9f6098dbc2c Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 28 Aug 2023 18:50:20 +0800 Subject: [PATCH 172/327] clean up --- .../deadline/plugins/publish/submit_max_deadline.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index f1127f60f2..d8725e853c 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -249,12 +249,8 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, if instance.data["renderer"] == "Redshift_Renderer": self.log.debug("Using Redshift...published scene wont be used..") replace_in_path = False - return replace_with_published_scene_path( - instance, replace_in_path) - else: - return replace_with_published_scene_path( - instance, replace_in_path) - + return replace_with_published_scene_path( + instance, replace_in_path) @staticmethod def _iter_expected_files(exp): From f95c4f27cee49f2281e427a6b855ca3364c99291 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 28 Aug 2023 13:48:20 +0200 Subject: [PATCH 173/327] Use `logging` module style formatting --- openpype/pipeline/publish/lib.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index 810e1dd342..815761cd0f 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -464,9 +464,8 @@ def apply_plugin_settings_automatically(plugin, settings, logger=None): for option, value in settings.items(): if logger: - logger.debug("Plugin {} - Attr: {} -> {}".format( - plugin.__name__, option, value - )) + logger.debug("Plugin %s - Attr: %s -> %s", + plugin.__name__, option, value) setattr(plugin, option, value) From d491b4f18be6bdc95bca2eb08077d8da1d5c79d5 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 28 Aug 2023 14:28:31 +0200 Subject: [PATCH 174/327] Optimize query (use `cmds.ls` once), add Select Invalid action, improve validation report, avoid "Unknown object type" errors --- .../validate_plugin_path_attributes.py | 53 ++++++++++++------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_plugin_path_attributes.py b/openpype/hosts/maya/plugins/publish/validate_plugin_path_attributes.py index 78334cd01f..9f47bf7a3d 100644 --- a/openpype/hosts/maya/plugins/publish/validate_plugin_path_attributes.py +++ b/openpype/hosts/maya/plugins/publish/validate_plugin_path_attributes.py @@ -4,6 +4,8 @@ from maya import cmds import pyblish.api +from openpype.hosts.maya.api.lib import pairwise +from openpype.hosts.maya.api.action import SelectInvalidAction from openpype.pipeline.publish import ( ValidateContentsOrder, PublishValidationError @@ -19,31 +21,33 @@ class ValidatePluginPathAttributes(pyblish.api.InstancePlugin): hosts = ['maya'] families = ["workfile"] label = "Plug-in Path Attributes" + actions = [SelectInvalidAction] - def get_invalid(self, instance): + # Attributes are defined in project settings + attribute = [] + + @classmethod + def get_invalid(cls, instance): invalid = list() - # get the project setting - validate_path = ( - instance.context.data["project_settings"]["maya"]["publish"] - ) - file_attr = validate_path["ValidatePluginPathAttributes"]["attribute"] + file_attr = cls.attribute if not file_attr: return invalid - # get the nodes and file attributes - for node, attr in file_attr.items(): - # check the related nodes - targets = cmds.ls(type=node) + # Consider only valid node types to avoid "Unknown object type" warning + all_node_types = set(cmds.allNodeTypes()) + node_types = [key for key in file_attr.keys() if key in all_node_types] - for target in targets: - # get the filepath - file_attr = "{}.{}".format(target, attr) - filepath = cmds.getAttr(file_attr) + for node, node_type in pairwise(cmds.ls(type=node_types, + showType=True)): + # get the filepath + file_attr = "{}.{}".format(node, file_attr[node_type]) + filepath = cmds.getAttr(file_attr) - if filepath and not os.path.exists(filepath): - self.log.error("File {0} not exists".format(filepath)) # noqa - invalid.append(target) + if filepath and not os.path.exists(filepath): + cls.log.error("{} '{}' uses non-existing filepath: {}" + .format(node_type, node, filepath)) + invalid.append(node) return invalid @@ -51,5 +55,16 @@ class ValidatePluginPathAttributes(pyblish.api.InstancePlugin): """Process all directories Set as Filenames in Non-Maya Nodes""" invalid = self.get_invalid(instance) if invalid: - raise PublishValidationError("Non-existent Path " - "found: {0}".format(invalid)) + raise PublishValidationError( + title="Plug-in Path Attributes", + message="Non-existent filepath found on nodes: {}".format( + ", ".join(invalid) + ), + description=( + "## Plug-in nodes use invalid filepaths\n" + "The workfile contains nodes from plug-ins that use " + "filepaths which do not exist.\n\n" + "Please make sure their filepaths are correct and the " + "files exist on disk." + ) + ) From afebe088370cb3170ff3147f7258c55224654096 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 28 Aug 2023 16:23:36 +0200 Subject: [PATCH 175/327] Refactor to PublishValidationError to allow the RepairAction to work + provide informational report message --- .../plugins/publish/validate_shape_zero.py | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_shape_zero.py b/openpype/hosts/maya/plugins/publish/validate_shape_zero.py index 7a7e9a0aee..c7af6a60db 100644 --- a/openpype/hosts/maya/plugins/publish/validate_shape_zero.py +++ b/openpype/hosts/maya/plugins/publish/validate_shape_zero.py @@ -7,6 +7,7 @@ from openpype.hosts.maya.api import lib from openpype.pipeline.publish import ( ValidateContentsOrder, RepairAction, + PublishValidationError ) @@ -67,5 +68,30 @@ class ValidateShapeZero(pyblish.api.Validator): invalid = self.get_invalid(instance) if invalid: - raise ValueError("Shapes found with non-zero component tweaks: " - "{0}".format(invalid)) + raise PublishValidationError( + title="Shape Component Tweaks", + message="Shapes found with non-zero component tweaks: '{}'" + "".format(", ".join(invalid)), + description=( + "## Shapes found with component tweaks\n" + "Shapes were detected that have component tweaks on their " + "components. Please remove the component tweaks to " + "continue.\n\n" + "### Repair\n" + "The repair action will try to *freeze* the component " + "tweaks into the shapes, which is usually the correct fix " + "if the mesh has no construction history (= has its " + "history deleted)."), + detail=( + "Maya allows to store component tweaks within shape nodes " + "which are applied between its `inMesh` and `outMesh` " + "connections resulting in the output of a shape node " + "differing from the input. We usually want to avoid this " + "for published meshes (in particular for Maya scenes) as " + "it can have unintended results when using these meshes " + "as intermediate meshes since it applies positional " + "differences without being visible edits in the node " + "graph.\n\n" + "These tweaks are traditionally stored in the `.pnts` " + "attribute of shapes.") + ) From 7a513cde9a48b6185f2dbc27a267c80aea2b4f64 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 28 Aug 2023 16:29:52 +0200 Subject: [PATCH 176/327] Remove Validate Instance Attributes The new publisher always updates and imprints the publish instances with the latest Attribute Definitions of the Creator, thus making this legacy validator redundant. Currently legacy instances do not work through the new publisher (they need to be converted to work correctly) and thus even for legacy workfiles this validator is redundant. --- .../publish/validate_instance_attributes.py | 60 ------------------- 1 file changed, 60 deletions(-) delete mode 100644 openpype/hosts/maya/plugins/publish/validate_instance_attributes.py diff --git a/openpype/hosts/maya/plugins/publish/validate_instance_attributes.py b/openpype/hosts/maya/plugins/publish/validate_instance_attributes.py deleted file mode 100644 index f870c9f8c4..0000000000 --- a/openpype/hosts/maya/plugins/publish/validate_instance_attributes.py +++ /dev/null @@ -1,60 +0,0 @@ -from maya import cmds - -import pyblish.api -from openpype.pipeline.publish import ( - ValidateContentsOrder, PublishValidationError, RepairAction -) -from openpype.pipeline import discover_legacy_creator_plugins -from openpype.hosts.maya.api.lib import imprint - - -class ValidateInstanceAttributes(pyblish.api.InstancePlugin): - """Validate Instance Attributes. - - New attributes can be introduced as new features come in. Old instances - will need to be updated with these attributes for the documentation to make - sense, and users do not have to recreate the instances. - """ - - order = ValidateContentsOrder - hosts = ["maya"] - families = ["*"] - label = "Instance Attributes" - plugins_by_family = { - p.family: p for p in discover_legacy_creator_plugins() - } - actions = [RepairAction] - - @classmethod - def get_missing_attributes(self, instance): - plugin = self.plugins_by_family[instance.data["family"]] - subset = instance.data["subset"] - asset = instance.data["asset"] - objset = instance.data["objset"] - - missing_attributes = {} - for key, value in plugin(subset, asset).data.items(): - if not cmds.objExists("{}.{}".format(objset, key)): - missing_attributes[key] = value - - return missing_attributes - - def process(self, instance): - objset = instance.data.get("objset") - if objset is None: - self.log.debug( - "Skipping {} because no objectset found.".format(instance) - ) - return - - missing_attributes = self.get_missing_attributes(instance) - if missing_attributes: - raise PublishValidationError( - "Missing attributes on {}:\n{}".format( - objset, missing_attributes - ) - ) - - @classmethod - def repair(cls, instance): - imprint(instance.data["objset"], cls.get_missing_attributes(instance)) From e809050f15c98e965b5561249e8c7b604a3e15c9 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 28 Aug 2023 16:54:39 +0200 Subject: [PATCH 177/327] Re-use existing select invalid and repair actions, provide more elaborate PublishValidationError report, plus fix "optional" support by using `OptionalPyblishPluginMixin` base class. --- .../publish/validate_instance_in_context.py | 135 +++++++----------- 1 file changed, 49 insertions(+), 86 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_instance_in_context.py b/openpype/hosts/maya/plugins/publish/validate_instance_in_context.py index b257add7e8..4ded57137c 100644 --- a/openpype/hosts/maya/plugins/publish/validate_instance_in_context.py +++ b/openpype/hosts/maya/plugins/publish/validate_instance_in_context.py @@ -3,94 +3,19 @@ from __future__ import absolute_import import pyblish.api +import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( - ValidateContentsOrder, PublishValidationError + RepairAction, + ValidateContentsOrder, + PublishValidationError, + OptionalPyblishPluginMixin ) from maya import cmds -class SelectInvalidInstances(pyblish.api.Action): - """Select invalid instances in Outliner.""" - - label = "Select Instances" - icon = "briefcase" - on = "failed" - - def process(self, context, plugin): - """Process invalid validators and select invalid instances.""" - # Get the errored instances - failed = [] - for result in context.data["results"]: - if ( - result["error"] is None - or result["instance"] is None - or result["instance"] in failed - or result["plugin"] != plugin - ): - continue - - failed.append(result["instance"]) - - # Apply pyblish.logic to get the instances for the plug-in - instances = pyblish.api.instances_by_plugin(failed, plugin) - - if instances: - self.log.info( - "Selecting invalid nodes: %s" % ", ".join( - [str(x) for x in instances] - ) - ) - self.select(instances) - else: - self.log.info("No invalid nodes found.") - self.deselect() - - def select(self, instances): - cmds.select(instances, replace=True, noExpand=True) - - def deselect(self): - cmds.select(deselect=True) - - -class RepairSelectInvalidInstances(pyblish.api.Action): - """Repair the instance asset.""" - - label = "Repair" - icon = "wrench" - on = "failed" - - def process(self, context, plugin): - # Get the errored instances - failed = [] - for result in context.data["results"]: - if result["error"] is None: - continue - if result["instance"] is None: - continue - if result["instance"] in failed: - continue - if result["plugin"] != plugin: - continue - - failed.append(result["instance"]) - - # Apply pyblish.logic to get the instances for the plug-in - instances = pyblish.api.instances_by_plugin(failed, plugin) - - context_asset = context.data["assetEntity"]["name"] - for instance in instances: - self.set_attribute(instance, context_asset) - - def set_attribute(self, instance, context_asset): - cmds.setAttr( - instance.data.get("name") + ".asset", - context_asset, - type="string" - ) - - -class ValidateInstanceInContext(pyblish.api.InstancePlugin): +class ValidateInstanceInContext(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Validator to check if instance asset match context asset. When working in per-shot style you always publish data in context of @@ -104,11 +29,49 @@ class ValidateInstanceInContext(pyblish.api.InstancePlugin): label = "Instance in same Context" optional = True hosts = ["maya"] - actions = [SelectInvalidInstances, RepairSelectInvalidInstances] + actions = [ + openpype.hosts.maya.api.action.SelectInvalidAction, RepairAction + ] def process(self, instance): + if not self.is_active(instance.data): + return + asset = instance.data.get("asset") - context_asset = instance.context.data["assetEntity"]["name"] - msg = "{} has asset {}".format(instance.name, asset) + context_asset = self.get_context_asset(instance) if asset != context_asset: - raise PublishValidationError(msg) + raise PublishValidationError( + message=( + "Instance '{}' publishes to different asset than current " + "context: {}. Current context: {}".format( + instance.name, asset, context_asset + ) + ), + description=( + "## Publishing to a different asset\n" + "There are publish instances present which are publishing " + "into a different asset than your current context.\n\n" + "Usually this is not what you want but there can be cases " + "where you might want to publish into another asset or " + "shot. If that's the case you can disable the validation " + "on the instance to ignore it." + ) + ) + + @classmethod + def get_invalid(cls, instance): + return [instance.data["instance_node"]] + + @classmethod + def repair(cls, instance): + context_asset = cls.get_context_asset(instance) + instance_node = instance.data["instance_node"] + cmds.setAttr( + "{}.asset".format(instance_node), + context_asset, + type="string" + ) + + @staticmethod + def get_context_asset(instance): + return instance.context.data["assetEntity"]["name"] From 0f904cb32a0b7283cafd344f0b33fd7b95e6bda7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 28 Aug 2023 17:23:19 +0200 Subject: [PATCH 178/327] adding default sequence frame data --- openpype/plugins/publish/collect_sequence_frame_data.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/plugins/publish/collect_sequence_frame_data.py b/openpype/plugins/publish/collect_sequence_frame_data.py index c200b245e9..241e7b9011 100644 --- a/openpype/plugins/publish/collect_sequence_frame_data.py +++ b/openpype/plugins/publish/collect_sequence_frame_data.py @@ -50,4 +50,7 @@ class CollectSequenceFrameData(pyblish.api.InstancePlugin): return { "frameStart": repres_frames[0], "frameEnd": repres_frames[-1], + "handleStart": 0, + "handleEnd": 0, + "fps": instance.context.data["projectEntity"]["data"]["fps"] } From e6bbb0c038abb0476d66cbc1277dd170343f684b Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Mon, 28 Aug 2023 22:47:18 +0300 Subject: [PATCH 179/327] add reset fbs to reset framerange --- openpype/hosts/houdini/api/lib.py | 5 +++++ openpype/hosts/houdini/api/pipeline.py | 6 ------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index b03f8c8fc1..55f4fd6197 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -474,6 +474,11 @@ def maintained_selection(): def reset_framerange(): """Set frame range to current asset""" + # Set new scene fps + fps = get_asset_fps() + print("Setting scene FPS to {}".format(int(fps))) + set_scene_fps(fps) + project_name = get_current_project_name() asset_name = get_current_asset_name() # Get the asset ID from the database for the asset of current context diff --git a/openpype/hosts/houdini/api/pipeline.py b/openpype/hosts/houdini/api/pipeline.py index 8a26bbb504..3c325edfa7 100644 --- a/openpype/hosts/houdini/api/pipeline.py +++ b/openpype/hosts/houdini/api/pipeline.py @@ -25,7 +25,6 @@ from openpype.lib import ( emit_event, ) -from .lib import get_asset_fps log = logging.getLogger("openpype.hosts.houdini") @@ -385,11 +384,6 @@ def _set_context_settings(): None """ - # Set new scene fps - fps = get_asset_fps() - print("Setting scene FPS to %i" % fps) - lib.set_scene_fps(fps) - lib.reset_framerange() From 3eb2cc21b2ae34405f6eda2539c73384e2e948b4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 29 Aug 2023 12:01:29 +0200 Subject: [PATCH 180/327] Update ayon-python-api (#5512) * query asset only if asset id is available * updated ayon api * fix subsets arguments --- openpype/client/server/entities.py | 8 +- openpype/tools/utils/tasks_widget.py | 2 +- .../vendor/python/common/ayon_api/__init__.py | 10 + .../vendor/python/common/ayon_api/_api.py | 20 ++ .../python/common/ayon_api/constants.py | 7 +- .../python/common/ayon_api/graphql_queries.py | 6 +- .../python/common/ayon_api/server_api.py | 236 +++++++++++++++--- .../vendor/python/common/ayon_api/version.py | 2 +- 8 files changed, 248 insertions(+), 43 deletions(-) diff --git a/openpype/client/server/entities.py b/openpype/client/server/entities.py index 9579f13add..39322627bb 100644 --- a/openpype/client/server/entities.py +++ b/openpype/client/server/entities.py @@ -83,10 +83,10 @@ def _get_subsets( project_name, subset_ids, subset_names, - folder_ids, - names_by_folder_ids, - active, - fields + folder_ids=folder_ids, + names_by_folder_ids=names_by_folder_ids, + active=active, + fields=fields, ): yield convert_v4_subset_to_v3(subset) diff --git a/openpype/tools/utils/tasks_widget.py b/openpype/tools/utils/tasks_widget.py index 8c0505223e..b554ed50d3 100644 --- a/openpype/tools/utils/tasks_widget.py +++ b/openpype/tools/utils/tasks_widget.py @@ -75,7 +75,7 @@ class TasksModel(QtGui.QStandardItemModel): def set_asset_id(self, asset_id): asset_doc = None - if self._context_is_valid(): + if asset_id and self._context_is_valid(): project_name = self._get_current_project() asset_doc = get_asset_by_id( project_name, asset_id, fields=["data.tasks"] diff --git a/openpype/vendor/python/common/ayon_api/__init__.py b/openpype/vendor/python/common/ayon_api/__init__.py index 027e7a3da2..dc3d361f46 100644 --- a/openpype/vendor/python/common/ayon_api/__init__.py +++ b/openpype/vendor/python/common/ayon_api/__init__.py @@ -48,6 +48,11 @@ from ._api import ( patch, delete, + get_timeout, + set_timeout, + get_max_retries, + set_max_retries, + get_event, get_events, dispatch_event, @@ -245,6 +250,11 @@ __all__ = ( "patch", "delete", + "get_timeout", + "set_timeout", + "get_max_retries", + "set_max_retries", + "get_event", "get_events", "dispatch_event", diff --git a/openpype/vendor/python/common/ayon_api/_api.py b/openpype/vendor/python/common/ayon_api/_api.py index 1d7b1837f1..22e137d6e5 100644 --- a/openpype/vendor/python/common/ayon_api/_api.py +++ b/openpype/vendor/python/common/ayon_api/_api.py @@ -474,6 +474,26 @@ def delete(*args, **kwargs): return con.delete(*args, **kwargs) +def get_timeout(*args, **kwargs): + con = get_server_api_connection() + return con.get_timeout(*args, **kwargs) + + +def set_timeout(*args, **kwargs): + con = get_server_api_connection() + return con.set_timeout(*args, **kwargs) + + +def get_max_retries(*args, **kwargs): + con = get_server_api_connection() + return con.get_max_retries(*args, **kwargs) + + +def set_max_retries(*args, **kwargs): + con = get_server_api_connection() + return con.set_max_retries(*args, **kwargs) + + def get_event(*args, **kwargs): con = get_server_api_connection() return con.get_event(*args, **kwargs) diff --git a/openpype/vendor/python/common/ayon_api/constants.py b/openpype/vendor/python/common/ayon_api/constants.py index eb1ace0590..eaeb77b607 100644 --- a/openpype/vendor/python/common/ayon_api/constants.py +++ b/openpype/vendor/python/common/ayon_api/constants.py @@ -1,18 +1,21 @@ # Environments where server url and api key are stored for global connection SERVER_URL_ENV_KEY = "AYON_SERVER_URL" SERVER_API_ENV_KEY = "AYON_API_KEY" +SERVER_TIMEOUT_ENV_KEY = "AYON_SERVER_TIMEOUT" +SERVER_RETRIES_ENV_KEY = "AYON_SERVER_RETRIES" + # Backwards compatibility SERVER_TOKEN_ENV_KEY = SERVER_API_ENV_KEY # --- User --- DEFAULT_USER_FIELDS = { - "roles", + "accessGroups", + "defaultAccessGroups", "name", "isService", "isManager", "isGuest", "isAdmin", - "defaultRoles", "createdAt", "active", "hasPassword", diff --git a/openpype/vendor/python/common/ayon_api/graphql_queries.py b/openpype/vendor/python/common/ayon_api/graphql_queries.py index f31134a04d..2435fc8a17 100644 --- a/openpype/vendor/python/common/ayon_api/graphql_queries.py +++ b/openpype/vendor/python/common/ayon_api/graphql_queries.py @@ -247,9 +247,11 @@ def products_graphql_query(fields): query = GraphQlQuery("ProductsQuery") project_name_var = query.add_variable("projectName", "String!") - folder_ids_var = query.add_variable("folderIds", "[String!]") product_ids_var = query.add_variable("productIds", "[String!]") product_names_var = query.add_variable("productNames", "[String!]") + folder_ids_var = query.add_variable("folderIds", "[String!]") + product_types_var = query.add_variable("productTypes", "[String!]") + statuses_var = query.add_variable("statuses", "[String!]") project_field = query.add_field("project") project_field.set_filter("name", project_name_var) @@ -258,6 +260,8 @@ def products_graphql_query(fields): products_field.set_filter("ids", product_ids_var) products_field.set_filter("names", product_names_var) products_field.set_filter("folderIds", folder_ids_var) + products_field.set_filter("productTypes", product_types_var) + products_field.set_filter("statuses", statuses_var) nested_fields = fields_to_dict(set(fields)) add_links_fields(products_field, nested_fields) diff --git a/openpype/vendor/python/common/ayon_api/server_api.py b/openpype/vendor/python/common/ayon_api/server_api.py index f2689e88dc..511a239a83 100644 --- a/openpype/vendor/python/common/ayon_api/server_api.py +++ b/openpype/vendor/python/common/ayon_api/server_api.py @@ -2,6 +2,7 @@ import os import re import io import json +import time import logging import collections import platform @@ -26,6 +27,8 @@ except ImportError: from json import JSONDecodeError as RequestsJSONDecodeError from .constants import ( + SERVER_TIMEOUT_ENV_KEY, + SERVER_RETRIES_ENV_KEY, DEFAULT_PRODUCT_TYPE_FIELDS, DEFAULT_PROJECT_FIELDS, DEFAULT_FOLDER_FIELDS, @@ -127,6 +130,8 @@ class RestApiResponse(object): @property def text(self): + if self._response is None: + return self.detail return self._response.text @property @@ -135,6 +140,8 @@ class RestApiResponse(object): @property def headers(self): + if self._response is None: + return {} return self._response.headers @property @@ -148,6 +155,8 @@ class RestApiResponse(object): @property def content(self): + if self._response is None: + return b"" return self._response.content @property @@ -339,7 +348,11 @@ class ServerAPI(object): variable value 'AYON_CERT_FILE' by default. create_session (Optional[bool]): Create session for connection if token is available. Default is True. + timeout (Optional[float]): Timeout for requests. + max_retries (Optional[int]): Number of retries for requests. """ + _default_timeout = 10.0 + _default_max_retries = 3 def __init__( self, @@ -352,6 +365,8 @@ class ServerAPI(object): ssl_verify=None, cert=None, create_session=True, + timeout=None, + max_retries=None, ): if not base_url: raise ValueError("Invalid server URL {}".format(str(base_url))) @@ -370,6 +385,13 @@ class ServerAPI(object): ) self._sender = sender + self._timeout = None + self._max_retries = None + + # Set timeout and max retries based on passed values + self.set_timeout(timeout) + self.set_max_retries(max_retries) + if ssl_verify is None: # Custom AYON env variable for CA file or 'True' # - that should cover most default behaviors in 'requests' @@ -474,6 +496,87 @@ class ServerAPI(object): ssl_verify = property(get_ssl_verify, set_ssl_verify) cert = property(get_cert, set_cert) + @classmethod + def get_default_timeout(cls): + """Default value for requests timeout. + + First looks for environment variable SERVER_TIMEOUT_ENV_KEY which + can affect timeout value. If not available then use class + attribute '_default_timeout'. + + Returns: + float: Timeout value in seconds. + """ + + try: + return float(os.environ.get(SERVER_TIMEOUT_ENV_KEY)) + except (ValueError, TypeError): + pass + + return cls._default_timeout + + @classmethod + def get_default_max_retries(cls): + """Default value for requests max retries. + + First looks for environment variable SERVER_RETRIES_ENV_KEY, which + can affect max retries value. If not available then use class + attribute '_default_max_retries'. + + Returns: + int: Max retries value. + """ + + try: + return int(os.environ.get(SERVER_RETRIES_ENV_KEY)) + except (ValueError, TypeError): + pass + + return cls._default_max_retries + + def get_timeout(self): + """Current value for requests timeout. + + Returns: + float: Timeout value in seconds. + """ + + return self._timeout + + def set_timeout(self, timeout): + """Change timeout value for requests. + + Args: + timeout (Union[float, None]): Timeout value in seconds. + """ + + if timeout is None: + timeout = self.get_default_timeout() + self._timeout = float(timeout) + + def get_max_retries(self): + """Current value for requests max retries. + + Returns: + int: Max retries value. + """ + + return self._max_retries + + def set_max_retries(self, max_retries): + """Change max retries value for requests. + + Args: + max_retries (Union[int, None]): Max retries value. + """ + + if max_retries is None: + max_retries = self.get_default_max_retries() + self._max_retries = int(max_retries) + + timeout = property(get_timeout, set_timeout) + max_retries = property(get_max_retries, set_max_retries) + @property def access_token(self): """Access token used for authorization to server. @@ -890,9 +993,17 @@ class ServerAPI(object): for attr, filter_value in filters.items(): query.set_variable_value(attr, filter_value) + # Backwards compatibility for server 0.3.x + # - will be removed in future releases + major, minor, _, _, _ = self.server_version_tuple + access_groups_field = "accessGroups" + if major == 0 and minor <= 3: + access_groups_field = "roles" + for parsed_data in query.continuous_query(self): for user in parsed_data["users"]: - user["roles"] = json.loads(user["roles"]) + user[access_groups_field] = json.loads( + user[access_groups_field]) yield user def get_user(self, username=None): @@ -1004,6 +1115,10 @@ class ServerAPI(object): logout_from_server(self._base_url, self._access_token) def _do_rest_request(self, function, url, **kwargs): + kwargs.setdefault("timeout", self.timeout) + max_retries = kwargs.get("max_retries", self.max_retries) + if max_retries < 1: + max_retries = 1 if self._session is None: # Validate token if was not yet validated # - ignore validation if we're in middle of @@ -1023,38 +1138,54 @@ class ServerAPI(object): elif isinstance(function, RequestType): function = self._session_functions_mapping[function] - try: - response = function(url, **kwargs) + response = None + new_response = None + for _ in range(max_retries): + try: + response = function(url, **kwargs) + break + + except ConnectionRefusedError: + # Server may be restarting + new_response = RestApiResponse( + None, + {"detail": "Unable to connect the server. Connection refused"} + ) + except requests.exceptions.Timeout: + # Connection timed out + new_response = RestApiResponse( + None, + {"detail": "Connection timed out."} + ) + except requests.exceptions.ConnectionError: + # Other connection error (ssl, etc) - does not make sense to + # try call server again + new_response = RestApiResponse( + None, + {"detail": "Unable to connect the server. Connection error"} + ) + break + + time.sleep(0.1) + + if new_response is not None: + return new_response + + content_type = response.headers.get("Content-Type") + if content_type == "application/json": + try: + new_response = RestApiResponse(response) + except JSONDecodeError: + new_response = RestApiResponse( + None, + { + "detail": "The response is not a JSON: {}".format( + response.text) + } + ) - except ConnectionRefusedError: - new_response = RestApiResponse( - None, - {"detail": "Unable to connect the server. Connection refused"} - ) - except requests.exceptions.ConnectionError: - new_response = RestApiResponse( - None, - {"detail": "Unable to connect the server. Connection error"} - ) else: - content_type = response.headers.get("Content-Type") - if content_type == "application/json": - try: - new_response = RestApiResponse(response) - except JSONDecodeError: - new_response = RestApiResponse( - None, - { - "detail": "The response is not a JSON: {}".format( - response.text) - } - ) - - elif content_type in ("image/jpeg", "image/png"): - new_response = RestApiResponse(response) - - else: - new_response = RestApiResponse(response) + new_response = RestApiResponse(response) self.log.debug("Response {}".format(str(new_response))) return new_response @@ -1747,7 +1878,15 @@ class ServerAPI(object): entity_type_defaults = DEFAULT_WORKFILE_INFO_FIELDS elif entity_type == "user": - entity_type_defaults = DEFAULT_USER_FIELDS + entity_type_defaults = set(DEFAULT_USER_FIELDS) + # Backwards compatibility for server 0.3.x + # - will be removed in future releases + major, minor, _, _, _ = self.server_version_tuple + if major == 0 and minor <= 3: + entity_type_defaults.discard("accessGroups") + entity_type_defaults.discard("defaultAccessGroups") + entity_type_defaults.add("roles") + entity_type_defaults.add("defaultRoles") else: raise ValueError("Unknown entity type \"{}\"".format(entity_type)) @@ -2124,7 +2263,12 @@ class ServerAPI(object): server. """ - result = self.get("desktop/dependency_packages") + endpoint = "desktop/dependencyPackages" + major, minor, _, _, _ = self.server_version_tuple + if major == 0 and minor <= 3: + endpoint = "desktop/dependency_packages" + + result = self.get(endpoint) result.raise_for_status() return result.data @@ -3810,6 +3954,8 @@ class ServerAPI(object): product_ids=None, product_names=None, folder_ids=None, + product_types=None, + statuses=None, names_by_folder_ids=None, active=True, fields=None, @@ -3828,6 +3974,10 @@ class ServerAPI(object): filtering. folder_ids (Optional[Iterable[str]]): Ids of task parents. Use 'None' if folder is direct child of project. + product_types (Optional[Iterable[str]]): Product types used for + filtering. + statuses (Optional[Iterable[str]]): Product statuses used for + filtering. names_by_folder_ids (Optional[dict[str, Iterable[str]]]): Product name filtering by folder id. active (Optional[bool]): Filter active/inactive products. @@ -3862,6 +4012,18 @@ class ServerAPI(object): if not filter_folder_ids: return + filter_product_types = None + if product_types is not None: + filter_product_types = set(product_types) + if not filter_product_types: + return + + filter_statuses = None + if statuses is not None: + filter_statuses = set(statuses) + if not filter_statuses: + return + # This will disable 'folder_ids' and 'product_names' filters # - maybe could be enhanced in future? if names_by_folder_ids is not None: @@ -3881,7 +4043,7 @@ class ServerAPI(object): fields = set(fields) | {"id"} if "attrib" in fields: fields.remove("attrib") - fields |= self.get_attributes_fields_for_type("folder") + fields |= self.get_attributes_fields_for_type("product") else: fields = self.get_default_fields_for_type("product") @@ -3908,6 +4070,12 @@ class ServerAPI(object): if filter_folder_ids: filters["folderIds"] = list(filter_folder_ids) + if filter_product_types: + filters["productTypes"] = list(filter_product_types) + + if filter_statuses: + filters["statuses"] = list(filter_statuses) + if product_ids: filters["productIds"] = list(product_ids) diff --git a/openpype/vendor/python/common/ayon_api/version.py b/openpype/vendor/python/common/ayon_api/version.py index df841e0829..f3826a6407 100644 --- a/openpype/vendor/python/common/ayon_api/version.py +++ b/openpype/vendor/python/common/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for Ayon server.""" -__version__ = "0.3.5" +__version__ = "0.4.1" From 61a8ff26f0d42577d4f19346242270ab56a75505 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 29 Aug 2023 15:06:52 +0200 Subject: [PATCH 181/327] General: Fix Validate Publish Dir Validator (#5534) * Fix using wrong key * Update docstrings --- openpype/plugins/publish/validate_publish_dir.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/openpype/plugins/publish/validate_publish_dir.py b/openpype/plugins/publish/validate_publish_dir.py index 2f41127548..ad5fd34434 100644 --- a/openpype/plugins/publish/validate_publish_dir.py +++ b/openpype/plugins/publish/validate_publish_dir.py @@ -7,12 +7,12 @@ from openpype.pipeline.publish import ( class ValidatePublishDir(pyblish.api.InstancePlugin): - """Validates if 'publishDir' is a project directory + """Validates if files are being published into a project directory - 'publishDir' is collected based on publish templates. In specific cases - ('source' template) source folder of items is used as a 'publishDir', this - validates if it is inside any project dir for the project. - (eg. files are not published from local folder, unaccessible for studio' + In specific cases ('source' template - in place publishing) source folder + of published items is used as a regular `publish` dir. + This validates if it is inside any project dir for the project. + (eg. files are not published from local folder, inaccessible for studio') """ @@ -44,6 +44,8 @@ class ValidatePublishDir(pyblish.api.InstancePlugin): anatomy = instance.context.data["anatomy"] + # original_dirname must be convertable to rootless path + # in other case it is path inside of root folder for the project success, _ = anatomy.find_root_template_from_path(original_dirname) formatting_data = { @@ -56,11 +58,12 @@ class ValidatePublishDir(pyblish.api.InstancePlugin): formatting_data=formatting_data) def _get_template_name_from_instance(self, instance): + """Find template which will be used during integration.""" project_name = instance.context.data["projectName"] host_name = instance.context.data["hostName"] anatomy_data = instance.data["anatomyData"] family = anatomy_data["family"] - family = self.family_mapping.get("family") or family + family = self.family_mapping.get(family) or family task_info = anatomy_data.get("task") or {} return get_publish_template_name( From 9f31075acd53bc378cf4c347a1802f06cb4167d1 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Tue, 29 Aug 2023 16:41:09 +0300 Subject: [PATCH 182/327] resolve BigRoy's conversation --- openpype/hosts/houdini/api/lib.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 55f4fd6197..28805dc015 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -22,9 +22,12 @@ log = logging.getLogger(__name__) JSON_PREFIX = "JSON:::" -def get_asset_fps(): +def get_asset_fps(asset_doc=None): """Return current asset fps.""" - return get_current_project_asset()["data"].get("fps") + + if asset_doc is None: + asset_doc = get_current_project_asset(fields=["data.fps"]) + return asset_doc["data"].get("fps") def set_id(node, unique_id, overwrite=False): @@ -472,19 +475,19 @@ def maintained_selection(): def reset_framerange(): - """Set frame range to current asset""" - - # Set new scene fps - fps = get_asset_fps() - print("Setting scene FPS to {}".format(int(fps))) - set_scene_fps(fps) + """Set frame range and FPS to current asset""" + # Get asset data project_name = get_current_project_name() asset_name = get_current_asset_name() # Get the asset ID from the database for the asset of current context asset_doc = get_asset_by_name(project_name, asset_name) asset_data = asset_doc["data"] + # Get FPS + fps = get_asset_fps(asset_doc) + + # Get Start and End Frames frame_start = asset_data.get("frameStart") frame_end = asset_data.get("frameEnd") @@ -498,8 +501,12 @@ def reset_framerange(): frame_start -= int(handle_start) frame_end += int(handle_end) + # Set frame range and FPS + print("Setting scene FPS to {}".format(int(fps))) + set_scene_fps(fps) hou.playbar.setFrameRange(frame_start, frame_end) hou.playbar.setPlaybackRange(frame_start, frame_end) + print("Setting current frame to {}".format(frame_start)) hou.setFrame(frame_start) From c64e6c1fca25f096b0827d7c2418d2e67ebd1c91 Mon Sep 17 00:00:00 2001 From: Mustafa Zarkash Date: Tue, 29 Aug 2023 16:52:23 +0300 Subject: [PATCH 183/327] Update get_asset_fps Co-authored-by: Roy Nieterau --- openpype/hosts/houdini/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 28805dc015..975f4e531e 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -27,7 +27,7 @@ def get_asset_fps(asset_doc=None): if asset_doc is None: asset_doc = get_current_project_asset(fields=["data.fps"]) - return asset_doc["data"].get("fps") + return asset_doc["data"]["fps"] def set_id(node, unique_id, overwrite=False): From 44df6d69a6ffbe9a73dc38849251dc95ad025528 Mon Sep 17 00:00:00 2001 From: Mustafa Zarkash Date: Tue, 29 Aug 2023 16:55:24 +0300 Subject: [PATCH 184/327] Delete print statment Co-authored-by: Roy Nieterau --- openpype/hosts/houdini/api/lib.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 975f4e531e..75c7ff9fee 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -506,7 +506,6 @@ def reset_framerange(): set_scene_fps(fps) hou.playbar.setFrameRange(frame_start, frame_end) hou.playbar.setPlaybackRange(frame_start, frame_end) - print("Setting current frame to {}".format(frame_start)) hou.setFrame(frame_start) From 6014cc6549d4869c81ff8cbe1acacfb30332db3a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 29 Aug 2023 16:16:31 +0200 Subject: [PATCH 185/327] Enhancement: Deadline plugins optimize, cleanup and fix optional support for validate deadline pools (#5531) * Fix optional support * Query deadline only once per url * Report both pools if both are invalid instead of only primary pool * Fix formatting in UI * Re-use existing implementation of `requests_get` * Cosmetics * Cache deadline url responses to avoid the need of request per instance * Only format error message when needed + convert to `KnownPublishError` * Allow deadline url per instance, similar to `ValidateDeadlineConnections` * Tweak grammar/readability * Fix title * Remove instance data from right side in Publish report since it's available in logs --- .../collect_deadline_server_from_instance.py | 14 +++-- .../publish/help/validate_deadline_pools.xml | 30 ++++----- .../publish/validate_deadline_connection.py | 34 ++++------ .../publish/validate_deadline_pools.py | 63 +++++++++++++------ 4 files changed, 79 insertions(+), 62 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/collect_deadline_server_from_instance.py b/openpype/modules/deadline/plugins/publish/collect_deadline_server_from_instance.py index eadfc3c83e..8a408d7f4f 100644 --- a/openpype/modules/deadline/plugins/publish/collect_deadline_server_from_instance.py +++ b/openpype/modules/deadline/plugins/publish/collect_deadline_server_from_instance.py @@ -8,6 +8,7 @@ attribute or using default server if that attribute doesn't exists. from maya import cmds import pyblish.api +from openpype.pipeline.publish import KnownPublishError class CollectDeadlineServerFromInstance(pyblish.api.InstancePlugin): @@ -81,13 +82,14 @@ class CollectDeadlineServerFromInstance(pyblish.api.InstancePlugin): if k in default_servers } - msg = ( - "\"{}\" server on instance is not enabled in project settings." - " Enabled project servers:\n{}".format( - instance_server, project_enabled_servers + if instance_server not in project_enabled_servers: + msg = ( + "\"{}\" server on instance is not enabled in project settings." + " Enabled project servers:\n{}".format( + instance_server, project_enabled_servers + ) ) - ) - assert instance_server in project_enabled_servers, msg + raise KnownPublishError(msg) self.log.debug("Using project approved server.") return project_enabled_servers[instance_server] diff --git a/openpype/modules/deadline/plugins/publish/help/validate_deadline_pools.xml b/openpype/modules/deadline/plugins/publish/help/validate_deadline_pools.xml index 0e7d72910e..aa21df3734 100644 --- a/openpype/modules/deadline/plugins/publish/help/validate_deadline_pools.xml +++ b/openpype/modules/deadline/plugins/publish/help/validate_deadline_pools.xml @@ -1,31 +1,31 @@ - Scene setting + Deadline Pools - ## Invalid Deadline pools found +## Invalid Deadline pools found - Configured pools don't match what is set in Deadline. +Configured pools don't match available pools in Deadline. - {invalid_value_str} +### How to repair? - ### How to repair? +If your instance had deadline pools set on creation, remove or +change them. - If your instance had deadline pools set on creation, remove or - change them. +In other cases inform admin to change them in Settings. - In other cases inform admin to change them in Settings. +Available deadline pools: + +{pools_str} - Available deadline pools {pools_str}. - ### __Detailed Info__ +### __Detailed Info__ - This error is shown when deadline pool is not on Deadline anymore. It - could happen in case of republish old workfile which was created with - previous deadline pools, - or someone changed pools on Deadline side, but didn't modify Openpype - Settings. +This error is shown when a configured pool is not available on Deadline. It +can happen when publishing old workfiles which were created with previous +deadline pools, or someone changed the available pools in Deadline, +but didn't modify Openpype Settings to match the changes. \ No newline at end of file diff --git a/openpype/modules/deadline/plugins/publish/validate_deadline_connection.py b/openpype/modules/deadline/plugins/publish/validate_deadline_connection.py index a30401e7dc..a7b300beff 100644 --- a/openpype/modules/deadline/plugins/publish/validate_deadline_connection.py +++ b/openpype/modules/deadline/plugins/publish/validate_deadline_connection.py @@ -1,8 +1,7 @@ -import os -import requests - import pyblish.api +from openpype_modules.deadline.abstract_submit_deadline import requests_get + class ValidateDeadlineConnection(pyblish.api.InstancePlugin): """Validate Deadline Web Service is running""" @@ -12,34 +11,25 @@ class ValidateDeadlineConnection(pyblish.api.InstancePlugin): hosts = ["maya", "nuke"] families = ["renderlayer", "render"] + # cache + responses = {} + def process(self, instance): # get default deadline webservice url from deadline module deadline_url = instance.context.data["defaultDeadline"] # if custom one is set in instance, use that if instance.data.get("deadlineUrl"): deadline_url = instance.data.get("deadlineUrl") - self.log.info( - "We have deadline URL on instance {}".format( - deadline_url)) + self.log.debug( + "We have deadline URL on instance {}".format(deadline_url) + ) assert deadline_url, "Requires Deadline Webservice URL" - # Check response - response = self._requests_get(deadline_url) + if deadline_url not in self.responses: + self.responses[deadline_url] = requests_get(deadline_url) + + response = self.responses[deadline_url] assert response.ok, "Response must be ok" assert response.text.startswith("Deadline Web Service "), ( "Web service did not respond with 'Deadline Web Service'" ) - - def _requests_get(self, *args, **kwargs): - """ Wrapper for requests, disabling SSL certificate validation if - DONT_VERIFY_SSL environment variable is found. This is useful when - Deadline or Muster server are running with self-signed certificates - and their certificate is not added to trusted certificates on - client machines. - - WARNING: disabling SSL certificate validation is defeating one line - of defense SSL is providing and it is not recommended. - """ - if 'verify' not in kwargs: - kwargs['verify'] = False if os.getenv("OPENPYPE_DONT_VERIFY_SSL", True) else True # noqa - return requests.get(*args, **kwargs) diff --git a/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py b/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py index 594f0ef866..949caff7d8 100644 --- a/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py +++ b/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py @@ -25,33 +25,58 @@ class ValidateDeadlinePools(OptionalPyblishPluginMixin, "maxrender"] optional = True + # cache + pools_per_url = {} + def process(self, instance): + if not self.is_active(instance.data): + return + if not instance.data.get("farm"): self.log.debug("Skipping local instance.") return - # get default deadline webservice url from deadline module - deadline_url = instance.context.data["defaultDeadline"] - self.log.info("deadline_url::{}".format(deadline_url)) - pools = DeadlineModule.get_deadline_pools(deadline_url, log=self.log) - self.log.info("pools::{}".format(pools)) - - formatting_data = { - "pools_str": ",".join(pools) - } + deadline_url = self.get_deadline_url(instance) + pools = self.get_pools(deadline_url) + invalid_pools = {} primary_pool = instance.data.get("primaryPool") if primary_pool and primary_pool not in pools: - msg = "Configured primary '{}' not present on Deadline".format( - instance.data["primaryPool"]) - formatting_data["invalid_value_str"] = msg - raise PublishXmlValidationError(self, msg, - formatting_data=formatting_data) + invalid_pools["primary"] = primary_pool secondary_pool = instance.data.get("secondaryPool") if secondary_pool and secondary_pool not in pools: - msg = "Configured secondary '{}' not present on Deadline".format( - instance.data["secondaryPool"]) - formatting_data["invalid_value_str"] = msg - raise PublishXmlValidationError(self, msg, - formatting_data=formatting_data) + invalid_pools["secondary"] = secondary_pool + + if invalid_pools: + message = "\n".join( + "{} pool '{}' not available on Deadline".format(key.title(), + pool) + for key, pool in invalid_pools.items() + ) + raise PublishXmlValidationError( + plugin=self, + message=message, + formatting_data={"pools_str": ", ".join(pools)} + ) + + def get_deadline_url(self, instance): + # get default deadline webservice url from deadline module + deadline_url = instance.context.data["defaultDeadline"] + if instance.data.get("deadlineUrl"): + # if custom one is set in instance, use that + deadline_url = instance.data.get("deadlineUrl") + return deadline_url + + def get_pools(self, deadline_url): + if deadline_url not in self.pools_per_url: + self.log.debug( + "Querying available pools for Deadline url: {}".format( + deadline_url) + ) + pools = DeadlineModule.get_deadline_pools(deadline_url, + log=self.log) + self.log.info("Available pools: {}".format(pools)) + self.pools_per_url[deadline_url] = pools + + return self.pools_per_url[deadline_url] From c157f74b498eb5ace1822792b5813e964ac79ebc Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 29 Aug 2023 16:45:09 +0200 Subject: [PATCH 186/327] Fix double spaces in message (#5190) --- openpype/plugins/publish/validate_version.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/plugins/publish/validate_version.py b/openpype/plugins/publish/validate_version.py index 2b919a3119..84d52fab73 100644 --- a/openpype/plugins/publish/validate_version.py +++ b/openpype/plugins/publish/validate_version.py @@ -25,16 +25,16 @@ class ValidateVersion(pyblish.api.InstancePlugin): # TODO: Remove full non-html version upon drop of old publisher msg = ( "Version '{0}' from instance '{1}' that you are " - " trying to publish is lower or equal to an existing version " - " in the database. Version in database: '{2}'." + "trying to publish is lower or equal to an existing version " + "in the database. Version in database: '{2}'." "Please version up your workfile to a higher version number " "than: '{2}'." ).format(version, instance.data["name"], latest_version) msg_html = ( "Version {0} from instance {1} that you are " - " trying to publish is lower or equal to an existing version " - " in the database. Version in database: {2}.

" + "trying to publish is lower or equal to an existing version " + "in the database. Version in database: {2}.

" "Please version up your workfile to a higher version number " "than: {2}." ).format(version, instance.data["name"], latest_version) From e56d3530cb7668bf92b78a61be61508b23ee89fb Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 29 Aug 2023 17:31:49 +0200 Subject: [PATCH 187/327] Chore: Queued event system (#5514) * implemented queued event system * implemented basic tests --- openpype/lib/events.py | 90 +++++++++++++++++++- tests/unit/openpype/lib/test_event_system.py | 83 ++++++++++++++++++ 2 files changed, 171 insertions(+), 2 deletions(-) create mode 100644 tests/unit/openpype/lib/test_event_system.py diff --git a/openpype/lib/events.py b/openpype/lib/events.py index dca58fcf93..496b765a05 100644 --- a/openpype/lib/events.py +++ b/openpype/lib/events.py @@ -3,6 +3,7 @@ import os import re import copy import inspect +import collections import logging import weakref from uuid import uuid4 @@ -340,8 +341,8 @@ class EventSystem(object): event.emit() return event - def emit_event(self, event): - """Emit event object. + def _process_event(self, event): + """Process event topic and trigger callbacks. Args: event (Event): Prepared event with topic and data. @@ -356,6 +357,91 @@ class EventSystem(object): for callback in invalid_callbacks: self._registered_callbacks.remove(callback) + def emit_event(self, event): + """Emit event object. + + Args: + event (Event): Prepared event with topic and data. + """ + + self._process_event(event) + + +class QueuedEventSystem(EventSystem): + """Events are automatically processed in queue. + + If callback triggers another event, the event is not processed until + all callbacks of previous event are processed. + + Allows to implement custom event process loop by changing 'auto_execute'. + + Note: + This probably should be default behavior of 'EventSystem'. Changing it + now could cause problems in existing code. + + Args: + auto_execute (Optional[bool]): If 'True', events are processed + automatically. Custom loop calling 'process_next_event' + must be implemented when set to 'False'. + """ + + def __init__(self, auto_execute=True): + super(QueuedEventSystem, self).__init__() + self._event_queue = collections.deque() + self._current_event = None + self._auto_execute = auto_execute + + def __len__(self): + return self.count() + + def count(self): + """Get number of events in queue. + + Returns: + int: Number of events in queue. + """ + + return len(self._event_queue) + + def process_next_event(self): + """Process next event in queue. + + Should be used only if 'auto_execute' is set to 'False'. Only single + event is processed. + + Returns: + Union[Event, None]: Processed event. + """ + + if self._current_event is not None: + raise ValueError("An event is already in progress.") + + if not self._event_queue: + return None + event = self._event_queue.popleft() + self._current_event = event + self._process_event(event) + self._current_event = None + return event + + def emit_event(self, event): + """Emit event object. + + Args: + event (Event): Prepared event with topic and data. + """ + + if not self._auto_execute or self._current_event is not None: + self._event_queue.append(event) + return + + self._event_queue.append(event) + while self._event_queue: + event = self._event_queue.popleft() + self._current_event = event + self._process_event(event) + self._current_event = None + class GlobalEventSystem: """Event system living in global scope of process. diff --git a/tests/unit/openpype/lib/test_event_system.py b/tests/unit/openpype/lib/test_event_system.py new file mode 100644 index 0000000000..aa3f929065 --- /dev/null +++ b/tests/unit/openpype/lib/test_event_system.py @@ -0,0 +1,83 @@ +from openpype.lib.events import EventSystem, QueuedEventSystem + + +def test_default_event_system(): + output = [] + expected_output = [3, 2, 1] + event_system = EventSystem() + + def callback_1(): + event_system.emit("topic.2", {}, None) + output.append(1) + + def callback_2(): + event_system.emit("topic.3", {}, None) + output.append(2) + + def callback_3(): + output.append(3) + + event_system.add_callback("topic.1", callback_1) + event_system.add_callback("topic.2", callback_2) + event_system.add_callback("topic.3", callback_3) + + event_system.emit("topic.1", {}, None) + + assert output == expected_output, ( + "Callbacks were not called in correct order") + + +def test_base_event_system_queue(): + output = [] + expected_output = [1, 2, 3] + event_system = QueuedEventSystem() + + def callback_1(): + event_system.emit("topic.2", {}, None) + output.append(1) + + def callback_2(): + event_system.emit("topic.3", {}, None) + output.append(2) + + def callback_3(): + output.append(3) + + event_system.add_callback("topic.1", callback_1) + event_system.add_callback("topic.2", callback_2) + event_system.add_callback("topic.3", callback_3) + + event_system.emit("topic.1", {}, None) + + assert output == expected_output, ( + "Callbacks were not called in correct order") + + +def test_manual_event_system_queue(): + output = [] + expected_output = [1, 2, 3] + event_system = QueuedEventSystem(auto_execute=False) + + def callback_1(): + event_system.emit("topic.2", {}, None) + output.append(1) + + def callback_2(): + event_system.emit("topic.3", {}, None) + output.append(2) + + def callback_3(): + output.append(3) + + event_system.add_callback("topic.1", callback_1) + event_system.add_callback("topic.2", callback_2) + event_system.add_callback("topic.3", callback_3) + + event_system.emit("topic.1", {}, None) + + while True: + if event_system.process_next_event() is None: + break + + assert output == expected_output, ( + "Callbacks were not called in correct order") From 04145020f6c41014697ec171d9ede8a389506dce Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 29 Aug 2023 17:35:48 +0200 Subject: [PATCH 188/327] Tests: fix unit tests (#5533) * Changed test zip file location * Updated tests and updated Unreal plugin Unreal plugin was previously ejected into general one, which was later removed as unnecessary. In Unreal plugin were kept fixed bugs from general one (slate issue, better name pattern for clique) * Updated raised exception type --- .../publish/validate_sequence_frames.py | 20 ++++++- .../publish/test_validate_sequence_frames.py | 53 +++++-------------- tests/unit/openpype/lib/test_delivery.py | 3 +- .../sync_server/test_site_operations.py | 9 ++-- 4 files changed, 39 insertions(+), 46 deletions(-) diff --git a/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py b/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py index 76bb25fac3..96485d5a2d 100644 --- a/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py +++ b/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py @@ -1,4 +1,6 @@ import clique +import os +import re import pyblish.api @@ -21,7 +23,19 @@ class ValidateSequenceFrames(pyblish.api.InstancePlugin): representations = instance.data.get("representations") for repr in representations: data = instance.data.get("assetEntity", {}).get("data", {}) - patterns = [clique.PATTERNS["frames"]] + repr_files = repr["files"] + if isinstance(repr_files, str): + continue + + ext = repr.get("ext") + if not ext: + _, ext = os.path.splitext(repr_files[0]) + elif not ext.startswith("."): + ext = ".{}".format(ext) + pattern = r"\D?(?P(?P0*)\d+){}$".format( + re.escape(ext)) + patterns = [pattern] + collections, remainder = clique.assemble( repr["files"], minimum_items=1, patterns=patterns) @@ -30,6 +44,10 @@ class ValidateSequenceFrames(pyblish.api.InstancePlugin): collection = collections[0] frames = list(collection.indexes) + if instance.data.get("slate"): + # Slate is not part of the frame range + frames = frames[1:] + current_range = (frames[0], frames[-1]) required_range = (data["clipIn"], data["clipOut"]) diff --git a/tests/unit/openpype/hosts/unreal/plugins/publish/test_validate_sequence_frames.py b/tests/unit/openpype/hosts/unreal/plugins/publish/test_validate_sequence_frames.py index 17e47c9f64..f472b8052a 100644 --- a/tests/unit/openpype/hosts/unreal/plugins/publish/test_validate_sequence_frames.py +++ b/tests/unit/openpype/hosts/unreal/plugins/publish/test_validate_sequence_frames.py @@ -19,7 +19,7 @@ import logging from pyblish.api import Instance as PyblishInstance from tests.lib.testing_classes import BaseTest -from openpype.plugins.publish.validate_sequence_frames import ( +from openpype.hosts.unreal.plugins.publish.validate_sequence_frames import ( ValidateSequenceFrames ) @@ -38,7 +38,13 @@ class TestValidateSequenceFrames(BaseTest): data = { "frameStart": 1001, "frameEnd": 1002, - "representations": [] + "representations": [], + "assetEntity": { + "data": { + "clipIn": 1001, + "clipOut": 1002, + } + } } yield Instance @@ -58,6 +64,7 @@ class TestValidateSequenceFrames(BaseTest): ] instance.data["representations"] = representations instance.data["frameEnd"] = 1001 + instance.data["assetEntity"]["data"]["clipOut"] = 1001 plugin.process(instance) @@ -84,49 +91,11 @@ class TestValidateSequenceFrames(BaseTest): plugin.process(instance) - @pytest.mark.parametrize("files", - [["Main_beauty.1001.v001.exr", - "Main_beauty.1002.v001.exr"]]) - def test_validate_sequence_frames_wrong_name(self, instance, - plugin, files): - # tests for names with number inside, caused clique failure before - representations = [ - { - "ext": "exr", - "files": files, - } - ] - instance.data["representations"] = representations - - with pytest.raises(AssertionError) as excinfo: - plugin.process(instance) - assert ("Must detect single collection" in - str(excinfo.value)) - - @pytest.mark.parametrize("files", - [["Main_beauty.v001.1001.ass.gz", - "Main_beauty.v001.1002.ass.gz"]]) - def test_validate_sequence_frames_possible_wrong_name( - self, instance, plugin, files): - # currently pattern fails on extensions with dots - representations = [ - { - "files": files, - } - ] - instance.data["representations"] = representations - - with pytest.raises(AssertionError) as excinfo: - plugin.process(instance) - assert ("Must not have remainder" in - str(excinfo.value)) - @pytest.mark.parametrize("files", [["Main_beauty.v001.1001.ass.gz", "Main_beauty.v001.1002.ass.gz"]]) def test_validate_sequence_frames__correct_ext( self, instance, plugin, files): - # currently pattern fails on extensions with dots representations = [ { "ext": "ass.gz", @@ -147,6 +116,7 @@ class TestValidateSequenceFrames(BaseTest): ] instance.data["representations"] = representations instance.data["frameEnd"] = 1003 + instance.data["assetEntity"]["data"]["clipOut"] = 1003 plugin.process(instance) @@ -160,6 +130,7 @@ class TestValidateSequenceFrames(BaseTest): ] instance.data["representations"] = representations instance.data["frameEnd"] = 1003 + instance.data["assetEntity"]["data"]["clipOut"] = 1003 with pytest.raises(ValueError) as excinfo: plugin.process(instance) @@ -175,6 +146,7 @@ class TestValidateSequenceFrames(BaseTest): ] instance.data["representations"] = representations instance.data["frameEnd"] = 1003 + instance.data["assetEntity"]["data"]["clipOut"] = 1003 with pytest.raises(AssertionError) as excinfo: plugin.process(instance) @@ -195,6 +167,7 @@ class TestValidateSequenceFrames(BaseTest): instance.data["slate"] = True instance.data["representations"] = representations instance.data["frameEnd"] = 1003 + instance.data["assetEntity"]["data"]["clipOut"] = 1003 plugin.process(instance) diff --git a/tests/unit/openpype/lib/test_delivery.py b/tests/unit/openpype/lib/test_delivery.py index 04a71655e3..f1e435f3f8 100644 --- a/tests/unit/openpype/lib/test_delivery.py +++ b/tests/unit/openpype/lib/test_delivery.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """Test suite for delivery functions.""" -from openpype.lib.delivery import collect_frames +from openpype.lib import collect_frames def test_collect_frames_multi_sequence(): @@ -153,4 +153,3 @@ def test_collect_frames_single_file(): print(ret) assert ret == expected, "Not matching" - diff --git a/tests/unit/openpype/modules/sync_server/test_site_operations.py b/tests/unit/openpype/modules/sync_server/test_site_operations.py index 6a861100a4..c4a83e33a6 100644 --- a/tests/unit/openpype/modules/sync_server/test_site_operations.py +++ b/tests/unit/openpype/modules/sync_server/test_site_operations.py @@ -12,16 +12,19 @@ removes temporary databases (?) """ import pytest +from bson.objectid import ObjectId from tests.lib.testing_classes import ModuleUnitTest -from bson.objectid import ObjectId + +from openpype.modules.sync_server.utils import SiteAlreadyPresentError + class TestSiteOperation(ModuleUnitTest): REPRESENTATION_ID = "60e578d0c987036c6a7b741d" - TEST_FILES = [("1eCwPljuJeOI8A3aisfOIBKKjcmIycTEt", + TEST_FILES = [("1FHE70Hi7y05LLT_1O3Y6jGxwZGXKV9zX", "test_site_operations.zip", '')] @pytest.fixture(scope="module") @@ -71,7 +74,7 @@ class TestSiteOperation(ModuleUnitTest): @pytest.mark.usefixtures("setup_sync_server_module") def test_add_site_again(self, dbcon, setup_sync_server_module): """Depends on test_add_site, must throw exception.""" - with pytest.raises(ValueError): + with pytest.raises(SiteAlreadyPresentError): setup_sync_server_module.add_site(self.TEST_PROJECT_NAME, self.REPRESENTATION_ID, site_name='test_site') From ed53ef12d5befa621965847016c375033e1b24b6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 29 Aug 2023 17:48:33 +0200 Subject: [PATCH 189/327] Chore: PowerShell script for docker build (#5535) * added powershell script to build using docker * fix empty variant * make sure build folder exists * added docker_build.ps1 to readme * Tweaked readme to include reason for docker_build.ps1 --------- Co-authored-by: Petr Kalis --- README.md | 4 ++ tools/docker_build.ps1 | 98 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 tools/docker_build.ps1 diff --git a/README.md b/README.md index 6caed8061c..92f1cb62dc 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,10 @@ sudo ./tools/docker_build.sh centos7 If all is successful, you'll find built OpenPype in `./build/` folder. +Docker build can be also started from Windows machine, just use `./tools/docker_build.ps1` instead of shell script. + +This could be used even for building linux build (with argument `centos7` or `debian`) + #### Manual build You will need [Python >= 3.9](https://www.python.org/downloads/) and [git](https://git-scm.com/downloads). You'll also need [curl](https://curl.se) on systems that doesn't have one preinstalled. diff --git a/tools/docker_build.ps1 b/tools/docker_build.ps1 new file mode 100644 index 0000000000..392165288c --- /dev/null +++ b/tools/docker_build.ps1 @@ -0,0 +1,98 @@ +$current_dir = Get-Location +$script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent +$repo_root = (Get-Item $script_dir).parent.FullName + +$env:PSModulePath = $env:PSModulePath + ";$($repo_root)\tools\modules\powershell" + +function Exit-WithCode($exitcode) { + # Only exit this host process if it's a child of another PowerShell parent process... + $parentPID = (Get-CimInstance -ClassName Win32_Process -Filter "ProcessId=$PID" | Select-Object -Property ParentProcessId).ParentProcessId + $parentProcName = (Get-CimInstance -ClassName Win32_Process -Filter "ProcessId=$parentPID" | Select-Object -Property Name).Name + if ('powershell.exe' -eq $parentProcName) { $host.SetShouldExit($exitcode) } + + exit $exitcode +} + +function Restore-Cwd() { + $tmp_current_dir = Get-Location + if ("$tmp_current_dir" -ne "$current_dir") { + Write-Color -Text ">>> ", "Restoring current directory" -Color Green, Gray + Set-Location -Path $current_dir + } +} + +function Get-Container { + if (-not (Test-Path -PathType Leaf -Path "$($repo_root)\build\docker-image.id")) { + Write-Color -Text "!!! ", "Docker command failed, cannot find image id." -Color Red, Yellow + Restore-Cwd + Exit-WithCode 1 + } + $id = Get-Content "$($repo_root)\build\docker-image.id" + Write-Color -Text ">>> ", "Creating container from image id ", "[", $id, "]" -Color Green, Gray, White, Cyan, White + $cid = docker create $id bash + if ($LASTEXITCODE -ne 0) { + Write-Color -Text "!!! ", "Cannot create container." -Color Red, Yellow + Restore-Cwd + Exit-WithCode 1 + } + return $cid +} + +function Change-Cwd() { + Set-Location -Path $repo_root +} + +function New-DockerBuild { + $version_file = Get-Content -Path "$($repo_root)\openpype\version.py" + $result = [regex]::Matches($version_file, '__version__ = "(?\d+\.\d+.\d+.*)"') + $openpype_version = $result[0].Groups['version'].Value + $startTime = [int][double]::Parse((Get-Date -UFormat %s)) + Write-Color -Text ">>> ", "Building OpenPype using Docker ..." -Color Green, Gray, White + $variant = $args[0] + if ($variant.Length -eq 0) { + $dockerfile = "$($repo_root)\Dockerfile" + } else { + $dockerfile = "$( $repo_root )\Dockerfile.$variant" + } + if (-not (Test-Path -PathType Leaf -Path $dockerfile)) { + Write-Color -Text "!!! ", "Dockerfile for specifed platform ", "[", $variant, "]", "doesn't exist." -Color Red, Yellow, Cyan, White, Cyan, Yellow + Restore-Cwd + Exit-WithCode 1 + } + Write-Color -Text ">>> ", "Using Dockerfile for ", "[ ", $variant, " ]" -Color Green, Gray, White, Cyan, White + + $build_dir = "$($repo_root)\build" + if (-not(Test-Path $build_dir)) { + New-Item -ItemType Directory -Path $build_dir + } + Write-Color -Text "--- ", "Cleaning build directory ..." -Color Yellow, Gray + try { + Remove-Item -Recurse -Force "$($build_dir)\*" + } catch { + Write-Color -Text "!!! ", "Cannot clean build directory, possibly because process is using it." -Color Red, Gray + Write-Color -Text $_.Exception.Message -Color Red + Exit-WithCode 1 + } + + Write-Color -Text ">>> ", "Running Docker build ..." -Color Green, Gray, White + docker build --pull --iidfile $repo_root/build/docker-image.id --build-arg BUILD_DATE=$(Get-Date -UFormat %Y-%m-%dT%H:%M:%SZ) --build-arg VERSION=$openpype_version -t pypeclub/openpype:$openpype_version -f $dockerfile . + if ($LASTEXITCODE -ne 0) { + Write-Color -Text "!!! ", "Docker command failed.", $LASTEXITCODE -Color Red, Yellow, Red + Restore-Cwd + Exit-WithCode 1 + } + Write-Color -Text ">>> ", "Copying build from container ..." -Color Green, Gray, White + $cid = Get-Container + + docker cp "$($cid):/opt/openpype/build/exe.linux-x86_64-3.9" "$($repo_root)/build" + docker cp "$($cid):/opt/openpype/build/build.log" "$($repo_root)/build" + + $endTime = [int][double]::Parse((Get-Date -UFormat %s)) + try { + New-BurntToastNotification -AppLogo "$openpype_root/openpype/resources/icons/openpype_icon.png" -Text "OpenPype build complete!", "All done in $( $endTime - $startTime ) secs. You will find OpenPype and build log in build directory." + } catch {} + Write-Color -Text "*** ", "All done in ", $($endTime - $startTime), " secs. You will find OpenPype and build log in ", "'.\build'", " directory." -Color Green, Gray, White, Gray, White, Gray +} + +Change-Cwd +New-DockerBuild $ARGS From 65bd128d510598b00b9b51f48a7eddc9ad519abc Mon Sep 17 00:00:00 2001 From: Mustafa Zarkash Date: Tue, 29 Aug 2023 19:03:58 +0300 Subject: [PATCH 190/327] Enhancement: Update houdini main menu (#5527) * update houdini main menu * add separator --- openpype/hosts/houdini/startup/MainMenuCommon.xml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/startup/MainMenuCommon.xml b/openpype/hosts/houdini/startup/MainMenuCommon.xml index 47a4653d5d..5818a117eb 100644 --- a/openpype/hosts/houdini/startup/MainMenuCommon.xml +++ b/openpype/hosts/houdini/startup/MainMenuCommon.xml @@ -2,7 +2,19 @@ - + + + + + + From 9ff5c071b3f7e0cc1bf7f002719565d192030e3a Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Tue, 29 Aug 2023 21:05:43 +0300 Subject: [PATCH 191/327] resolve conversations --- openpype/hosts/houdini/api/colorspace.py | 18 +++++++++++++- .../houdini/plugins/create/create_review.py | 24 +++---------------- .../publish/validate_review_colorspace.py | 19 ++++----------- openpype/pipeline/colorspace.py | 6 ++--- 4 files changed, 28 insertions(+), 39 deletions(-) diff --git a/openpype/hosts/houdini/api/colorspace.py b/openpype/hosts/houdini/api/colorspace.py index 7047644225..5c3c605cd1 100644 --- a/openpype/hosts/houdini/api/colorspace.py +++ b/openpype/hosts/houdini/api/colorspace.py @@ -1,7 +1,7 @@ import attr import hou from openpype.hosts.houdini.api.lib import get_color_management_preferences - +from openpype.pipeline.colorspace import get_display_view_colorspace_name @attr.s class LayerMetadata(object): @@ -54,3 +54,19 @@ class ARenderProduct(object): ) ] return colorspace_data + + +def get_default_display_view_colorspace(): + """Get default display view colorspace. + + It's used for 'ociocolorspace' parm in OpneGL Node.""" + + data = get_color_management_preferences() + config_path = data.get("config") + display = data.get("display") + view = data.get("view") + + default_view_space = get_display_view_colorspace_name(config_path, + display, + view) + return default_view_space diff --git a/openpype/hosts/houdini/plugins/create/create_review.py b/openpype/hosts/houdini/plugins/create/create_review.py index 797116aaca..75a92e5e77 100644 --- a/openpype/hosts/houdini/plugins/create/create_review.py +++ b/openpype/hosts/houdini/plugins/create/create_review.py @@ -2,6 +2,7 @@ """Creator plugin for creating openGL reviews.""" from openpype.hosts.houdini.api import plugin from openpype.lib import EnumDef, BoolDef, NumberDef +from openpype.hosts.houdini.api.colorspace import get_default_display_view_colorspace import os import hou @@ -142,9 +143,8 @@ class CreateReview(plugin.HoudiniCreator): # to OpenColorIO instance_node.setParms({"colorcorrect": 2}) - self.log.debug("Get default view colorspace name..") - - default_view_space = self.get_default_view_space() + # Get default view space for ociocolorspace parm. + default_view_space = get_default_display_view_colorspace() instance_node.setParms( {"ociocolorspace": default_view_space} ) @@ -154,21 +154,3 @@ class CreateReview(plugin.HoudiniCreator): "the default view color space '{}'" .format(instance_node, default_view_space) ) - - return default_view_space - - def get_default_view_space(self): - """Get default view space for ociocolorspace parm.""" - - from openpype.pipeline.colorspace import get_display_view_colorspace_name # noqa - from openpype.hosts.houdini.api.lib import get_color_management_preferences # noqa - - data = get_color_management_preferences() - config_path = data.get("config") - display = data.get("display") - view = data.get("view") - - default_view_space = get_display_view_colorspace_name(config_path, - display, view) - - return default_view_space diff --git a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py index 5390b6b52f..09e4a489d2 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py +++ b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py @@ -3,6 +3,7 @@ import pyblish.api from openpype.pipeline import PublishValidationError from openpype.pipeline.publish import RepairAction from openpype.hosts.houdini.api.action import SelectROPAction +from openpype.hosts.houdini.api.colorspace import get_default_display_view_colorspace import os import hou @@ -38,7 +39,7 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin): rop_node = hou.node(instance.data["instance_node"]) if os.getenv("OCIO") is None: - cls.log.warning( + cls.log.debug( "Default Houdini colorspace is used, " " skipping check.." ) @@ -71,25 +72,15 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin): used to set colorspace on opengl node to the default view. """ - from openpype.pipeline.colorspace import get_display_view_colorspace_name # noqa - from openpype.hosts.houdini.api.lib import get_color_management_preferences # noqa - rop_node = hou.node(instance.data["instance_node"]) - data = get_color_management_preferences() - config_path = data.get("config") - display = data.get("display") - view = data.get("view") - - cls.log.debug("Get default view colorspace name..") - - default_view_space = get_display_view_colorspace_name(config_path, - display, view) + # Get default view colorspace name + default_view_space = get_default_display_view_colorspace() rop_node.setParms({"ociocolorspace": default_view_space}) cls.log.debug( "'OCIO Colorspace' parm on '{}' has been set to " "the default view color space '{}'" - .formate(rop_node, default_view_space) + .format(rop_node, default_view_space) ) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index a0efb5e18c..37974f4a0b 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -658,6 +658,6 @@ def get_display_view_colorspace_subprocess(config_path, display, view): run_openpype_process(*args, **process_kwargs) - # return all colorspaces - return_json_data = open(tmp_json_path).read() - return json.loads(return_json_data) + # return default view colorspace name + with open(tmp_json_path, "r") as f: + return json.load(f) From c799ae42eab8be32051ee373bdb71176002a20b8 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Tue, 29 Aug 2023 21:06:36 +0300 Subject: [PATCH 192/327] add spaces --- openpype/hosts/houdini/api/colorspace.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/api/colorspace.py b/openpype/hosts/houdini/api/colorspace.py index 5c3c605cd1..2662a968e2 100644 --- a/openpype/hosts/houdini/api/colorspace.py +++ b/openpype/hosts/houdini/api/colorspace.py @@ -67,6 +67,6 @@ def get_default_display_view_colorspace(): view = data.get("view") default_view_space = get_display_view_colorspace_name(config_path, - display, - view) + display, + view) return default_view_space From 75673151a6374591067c5e8f356ee3bb93706961 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Tue, 29 Aug 2023 21:18:27 +0300 Subject: [PATCH 193/327] resolve hound conversations --- openpype/hosts/houdini/plugins/create/create_review.py | 2 +- .../hosts/houdini/plugins/publish/validate_review_colorspace.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_review.py b/openpype/hosts/houdini/plugins/create/create_review.py index 75a92e5e77..c087c54f6c 100644 --- a/openpype/hosts/houdini/plugins/create/create_review.py +++ b/openpype/hosts/houdini/plugins/create/create_review.py @@ -2,7 +2,7 @@ """Creator plugin for creating openGL reviews.""" from openpype.hosts.houdini.api import plugin from openpype.lib import EnumDef, BoolDef, NumberDef -from openpype.hosts.houdini.api.colorspace import get_default_display_view_colorspace +from openpype.hosts.houdini.api.colorspace import get_default_display_view_colorspace # noqa import os import hou diff --git a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py index 09e4a489d2..2c7420bf48 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py +++ b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py @@ -3,7 +3,7 @@ import pyblish.api from openpype.pipeline import PublishValidationError from openpype.pipeline.publish import RepairAction from openpype.hosts.houdini.api.action import SelectROPAction -from openpype.hosts.houdini.api.colorspace import get_default_display_view_colorspace +from openpype.hosts.houdini.api.colorspace import get_default_display_view_colorspace # noqa import os import hou From 948687e7a236a3767b07c78eba07a13663234be0 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 30 Aug 2023 03:24:53 +0000 Subject: [PATCH 194/327] [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 6d89e1eeae..12f797228b 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.16.5-nightly.2" +__version__ = "3.16.5-nightly.3" From e426aca7213a32fcde9ca5b7e923444e29a54049 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 30 Aug 2023 03:25:28 +0000 Subject: [PATCH 195/327] 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 d7e49de5cb..669bf391cd 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.16.5-nightly.3 - 3.16.5-nightly.2 - 3.16.5-nightly.1 - 3.16.4 @@ -134,7 +135,6 @@ body: - 3.14.8 - 3.14.8-nightly.4 - 3.14.8-nightly.3 - - 3.14.8-nightly.2 validations: required: true - type: dropdown From 2b951d29f23916e3354ecb6a328452dbf24d5282 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 30 Aug 2023 09:50:59 +0200 Subject: [PATCH 196/327] Maya: Tweak logging for artist-facing reports in publisher --- .../maya/plugins/publish/collect_assembly.py | 3 -- .../maya/plugins/publish/collect_look.py | 29 ++++++++------- .../publish/collect_multiverse_look.py | 20 +++++----- .../maya/plugins/publish/collect_render.py | 12 +++--- .../publish/collect_render_layer_aovs.py | 6 +-- .../publish/collect_renderable_camera.py | 9 +++-- .../publish/collect_unreal_staticmesh.py | 4 +- .../maya/plugins/publish/collect_xgen.py | 2 +- .../maya/plugins/publish/collect_yeti_rig.py | 5 +-- .../publish/extract_arnold_scene_source.py | 10 ++--- .../maya/plugins/publish/extract_assembly.py | 2 +- .../plugins/publish/extract_camera_alembic.py | 4 +- .../publish/extract_camera_mayaScene.py | 8 ++-- .../hosts/maya/plugins/publish/extract_fbx.py | 8 ++-- .../maya/plugins/publish/extract_gltf.py | 7 +--- .../maya/plugins/publish/extract_gpu_cache.py | 2 +- .../publish/extract_import_reference.py | 12 +++--- .../maya/plugins/publish/extract_layout.py | 8 ++-- .../maya/plugins/publish/extract_look.py | 37 +++++++++---------- .../plugins/publish/extract_maya_scene_raw.py | 7 ++-- .../maya/plugins/publish/extract_model.py | 7 ++-- .../publish/extract_multiverse_look.py | 8 ++-- .../plugins/publish/extract_multiverse_usd.py | 13 +++---- .../publish/extract_multiverse_usd_comp.py | 10 ++--- .../publish/extract_multiverse_usd_over.py | 8 ++-- .../hosts/maya/plugins/publish/extract_obj.py | 8 ++-- .../maya/plugins/publish/extract_playblast.py | 8 ++-- .../plugins/publish/extract_pointcache.py | 4 +- .../maya/plugins/publish/extract_proxy_abc.py | 4 +- .../plugins/publish/extract_redshift_proxy.py | 6 +-- .../plugins/publish/extract_rendersetup.py | 2 +- .../hosts/maya/plugins/publish/extract_rig.py | 6 +-- .../maya/plugins/publish/extract_thumbnail.py | 6 +-- .../extract_unreal_skeletalmesh_abc.py | 10 ++--- .../extract_unreal_skeletalmesh_fbx.py | 10 ++--- .../publish/extract_unreal_staticmesh.py | 10 ++--- .../maya/plugins/publish/extract_vrayproxy.py | 6 +-- .../maya/plugins/publish/extract_vrayscene.py | 17 ++++----- .../plugins/publish/extract_workfile_xgen.py | 2 +- .../maya/plugins/publish/extract_xgen.py | 4 +- .../plugins/publish/extract_yeti_cache.py | 8 ++-- .../maya/plugins/publish/extract_yeti_rig.py | 12 +++--- .../plugins/publish/reset_xgen_attributes.py | 4 +- .../plugins/publish/submit_maya_muster.py | 8 ++-- .../plugins/publish/validate_assembly_name.py | 2 +- .../publish/validate_assembly_namespaces.py | 2 +- .../plugins/publish/validate_frame_range.py | 4 +- .../plugins/publish/validate_glsl_material.py | 14 +++---- .../publish/validate_instancer_content.py | 2 +- .../validate_instancer_frame_ranges.py | 9 +---- .../plugins/publish/validate_model_name.py | 2 +- .../publish/validate_mvlook_contents.py | 13 ++++--- .../validate_skeletalmesh_hierarchy.py | 2 +- .../validate_unreal_staticmesh_naming.py | 6 +-- .../validate_vray_distributed_rendering.py | 4 +- .../validate_yeti_renderscript_callbacks.py | 4 +- .../validate_yeti_rig_input_in_instance.py | 4 +- 57 files changed, 217 insertions(+), 227 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_assembly.py b/openpype/hosts/maya/plugins/publish/collect_assembly.py index 2aef9ab908..f64d6bee44 100644 --- a/openpype/hosts/maya/plugins/publish/collect_assembly.py +++ b/openpype/hosts/maya/plugins/publish/collect_assembly.py @@ -35,14 +35,11 @@ class CollectAssembly(pyblish.api.InstancePlugin): # Get all content from the instance instance_lookup = set(cmds.ls(instance, type="transform", long=True)) data = defaultdict(list) - self.log.info(instance_lookup) hierarchy_nodes = [] for container in containers: - self.log.info(container) root = lib.get_container_transforms(container, root=True) - self.log.info(root) if not root or root not in instance_lookup: continue diff --git a/openpype/hosts/maya/plugins/publish/collect_look.py b/openpype/hosts/maya/plugins/publish/collect_look.py index b3da920566..a2c3d6acbf 100644 --- a/openpype/hosts/maya/plugins/publish/collect_look.py +++ b/openpype/hosts/maya/plugins/publish/collect_look.py @@ -356,8 +356,9 @@ class CollectLook(pyblish.api.InstancePlugin): # Thus the data will be limited to only what we need. self.log.debug("obj_set {}".format(sets[obj_set])) if not sets[obj_set]["members"]: - self.log.info( - "Removing redundant set information: {}".format(obj_set)) + self.log.debug( + "Removing redundant set information: {}".format(obj_set) + ) sets.pop(obj_set, None) self.log.debug("Gathering attribute changes to instance members..") @@ -396,9 +397,9 @@ class CollectLook(pyblish.api.InstancePlugin): if con: materials.extend(con) - self.log.info("Found materials:\n{}".format(materials)) + self.log.debug("Found materials:\n{}".format(materials)) - self.log.info("Found the following sets:\n{}".format(look_sets)) + self.log.debug("Found the following sets:\n{}".format(look_sets)) # Get the entire node chain of the look sets # history = cmds.listHistory(look_sets) history = [] @@ -456,7 +457,7 @@ class CollectLook(pyblish.api.InstancePlugin): instance.extend(shader for shader in look_sets if shader not in instance_lookup) - self.log.info("Collected look for %s" % instance) + self.log.debug("Collected look for %s" % instance) def collect_sets(self, instance): """Collect all objectSets which are of importance for publishing @@ -593,7 +594,7 @@ class CollectLook(pyblish.api.InstancePlugin): if attribute == "fileTextureName": computed_attribute = node + ".computedFileTextureNamePattern" - self.log.info(" - file source: {}".format(source)) + self.log.debug(" - file source: {}".format(source)) color_space_attr = "{}.colorSpace".format(node) try: color_space = cmds.getAttr(color_space_attr) @@ -621,7 +622,7 @@ class CollectLook(pyblish.api.InstancePlugin): dependNode=True) ) if not source and cmds.nodeType(node) in pxr_nodes: - self.log.info("Renderman: source is empty, skipping...") + self.log.debug("Renderman: source is empty, skipping...") continue # We replace backslashes with forward slashes because V-Ray # can't handle the UDIM files with the backslashes in the @@ -630,14 +631,14 @@ class CollectLook(pyblish.api.InstancePlugin): files = get_file_node_files(node) if len(files) == 0: - self.log.error("No valid files found from node `%s`" % node) + self.log.debug("No valid files found from node `%s`" % node) - self.log.info("collection of resource done:") - self.log.info(" - node: {}".format(node)) - self.log.info(" - attribute: {}".format(attribute)) - self.log.info(" - source: {}".format(source)) - self.log.info(" - file: {}".format(files)) - self.log.info(" - color space: {}".format(color_space)) + self.log.debug("collection of resource done:") + self.log.debug(" - node: {}".format(node)) + self.log.debug(" - attribute: {}".format(attribute)) + self.log.debug(" - source: {}".format(source)) + self.log.debug(" - file: {}".format(files)) + self.log.debug(" - color space: {}".format(color_space)) # Define the resource yield { diff --git a/openpype/hosts/maya/plugins/publish/collect_multiverse_look.py b/openpype/hosts/maya/plugins/publish/collect_multiverse_look.py index 33fc7a025f..f05fb76d48 100644 --- a/openpype/hosts/maya/plugins/publish/collect_multiverse_look.py +++ b/openpype/hosts/maya/plugins/publish/collect_multiverse_look.py @@ -268,7 +268,7 @@ class CollectMultiverseLookData(pyblish.api.InstancePlugin): cmds.loadPlugin("MultiverseForMaya", quiet=True) import multiverse - self.log.info("Processing mvLook for '{}'".format(instance)) + self.log.debug("Processing mvLook for '{}'".format(instance)) nodes = set() for node in instance: @@ -287,7 +287,7 @@ class CollectMultiverseLookData(pyblish.api.InstancePlugin): publishMipMap = instance.data["publishMipMap"] for node in nodes: - self.log.info("Getting resources for '{}'".format(node)) + self.log.debug("Getting resources for '{}'".format(node)) # We know what nodes need to be collected, now we need to # extract the materials overrides. @@ -380,12 +380,12 @@ class CollectMultiverseLookData(pyblish.api.InstancePlugin): if len(files) == 0: self.log.error("No valid files found from node `%s`" % node) - self.log.info("collection of resource done:") - self.log.info(" - node: {}".format(node)) - self.log.info(" - attribute: {}".format(fname_attrib)) - self.log.info(" - source: {}".format(source)) - self.log.info(" - file: {}".format(files)) - self.log.info(" - color space: {}".format(color_space)) + self.log.debug("collection of resource done:") + self.log.debug(" - node: {}".format(node)) + self.log.debug(" - attribute: {}".format(fname_attrib)) + self.log.debug(" - source: {}".format(source)) + self.log.debug(" - file: {}".format(files)) + self.log.debug(" - color space: {}".format(color_space)) # Define the resource resource = {"node": node, @@ -406,14 +406,14 @@ class CollectMultiverseLookData(pyblish.api.InstancePlugin): extra_files = [] self.log.debug("Expecting MipMaps, going to look for them.") for fname in files: - self.log.info("Checking '{}' for mipmaps".format(fname)) + self.log.debug("Checking '{}' for mipmaps".format(fname)) if is_mipmap(fname): self.log.debug(" - file is already MipMap, skipping.") continue mipmap = get_mipmap(fname) if mipmap: - self.log.info(" mipmap found for '{}'".format(fname)) + self.log.debug(" mipmap found for '{}'".format(fname)) extra_files.append(mipmap) else: self.log.warning(" no mipmap found for '{}'".format(fname)) diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index c17a8789e4..82392f67bd 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -105,7 +105,7 @@ class CollectMayaRender(pyblish.api.InstancePlugin): "family": cmds.getAttr("{}.family".format(s)), } ) - self.log.info(" -> attach render to: {}".format(s)) + self.log.debug(" -> attach render to: {}".format(s)) layer_name = layer.name() @@ -137,10 +137,10 @@ class CollectMayaRender(pyblish.api.InstancePlugin): has_cameras = any(product.camera for product in render_products) assert has_cameras, "No render cameras found." - self.log.info("multipart: {}".format( + self.log.debug("multipart: {}".format( multipart)) assert expected_files, "no file names were generated, this is a bug" - self.log.info( + self.log.debug( "expected files: {}".format( json.dumps(expected_files, indent=4, sort_keys=True) ) @@ -175,7 +175,7 @@ class CollectMayaRender(pyblish.api.InstancePlugin): publish_meta_path = os.path.dirname(full_path) aov_dict[aov_first_key] = full_paths full_exp_files = [aov_dict] - self.log.info(full_exp_files) + self.log.debug(full_exp_files) if publish_meta_path is None: raise KnownPublishError("Unable to detect any expected output " @@ -227,7 +227,7 @@ class CollectMayaRender(pyblish.api.InstancePlugin): if platform.system().lower() in ["linux", "darwin"]: common_publish_meta_path = "/" + common_publish_meta_path - self.log.info( + self.log.debug( "Publish meta path: {}".format(common_publish_meta_path)) # Get layer specific settings, might be overrides @@ -300,7 +300,7 @@ class CollectMayaRender(pyblish.api.InstancePlugin): ) if rr_settings["enabled"]: data["rrPathName"] = instance.data.get("rrPathName") - self.log.info(data["rrPathName"]) + self.log.debug(data["rrPathName"]) if self.sync_workfile_version: data["version"] = context.data["version"] diff --git a/openpype/hosts/maya/plugins/publish/collect_render_layer_aovs.py b/openpype/hosts/maya/plugins/publish/collect_render_layer_aovs.py index c3dc31ead9..035c531a9b 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render_layer_aovs.py +++ b/openpype/hosts/maya/plugins/publish/collect_render_layer_aovs.py @@ -37,7 +37,7 @@ class CollectRenderLayerAOVS(pyblish.api.InstancePlugin): # Get renderer renderer = instance.data["renderer"] - self.log.info("Renderer found: {}".format(renderer)) + self.log.debug("Renderer found: {}".format(renderer)) rp_node_types = {"vray": ["VRayRenderElement", "VRayRenderElementSet"], "arnold": ["aiAOV"], @@ -66,8 +66,8 @@ class CollectRenderLayerAOVS(pyblish.api.InstancePlugin): result.append(render_pass) - self.log.info("Found {} render elements / AOVs for " - "'{}'".format(len(result), instance.data["subset"])) + self.log.debug("Found {} render elements / AOVs for " + "'{}'".format(len(result), instance.data["subset"])) instance.data["renderPasses"] = result diff --git a/openpype/hosts/maya/plugins/publish/collect_renderable_camera.py b/openpype/hosts/maya/plugins/publish/collect_renderable_camera.py index d1c3cf3b2c..4443e2e0db 100644 --- a/openpype/hosts/maya/plugins/publish/collect_renderable_camera.py +++ b/openpype/hosts/maya/plugins/publish/collect_renderable_camera.py @@ -21,11 +21,12 @@ class CollectRenderableCamera(pyblish.api.InstancePlugin): else: layer = instance.data["renderlayer"] - self.log.info("layer: {}".format(layer)) cameras = cmds.ls(type="camera", long=True) - renderable = [c for c in cameras if - get_attr_in_layer("%s.renderable" % c, layer)] + renderable = [cam for cam in cameras if + get_attr_in_layer("{}.renderable".format(cam), layer)] - self.log.info("Found cameras %s: %s" % (len(renderable), renderable)) + self.log.debug( + "Found renderable cameras %s: %s", len(renderable), renderable + ) instance.data["cameras"] = renderable diff --git a/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py b/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py index 79d0856fa0..03b6c4a188 100644 --- a/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/publish/collect_unreal_staticmesh.py @@ -19,7 +19,7 @@ class CollectUnrealStaticMesh(pyblish.api.InstancePlugin): instance.data["geometryMembers"] = cmds.sets( geometry_set, query=True) - self.log.info("geometry: {}".format( + self.log.debug("geometry: {}".format( pformat(instance.data.get("geometryMembers")))) collision_set = [ @@ -29,7 +29,7 @@ class CollectUnrealStaticMesh(pyblish.api.InstancePlugin): instance.data["collisionMembers"] = cmds.sets( collision_set, query=True) - self.log.info("collisions: {}".format( + self.log.debug("collisions: {}".format( pformat(instance.data.get("collisionMembers")))) frame = cmds.currentTime(query=True) diff --git a/openpype/hosts/maya/plugins/publish/collect_xgen.py b/openpype/hosts/maya/plugins/publish/collect_xgen.py index 46968f7d1a..45648e1776 100644 --- a/openpype/hosts/maya/plugins/publish/collect_xgen.py +++ b/openpype/hosts/maya/plugins/publish/collect_xgen.py @@ -67,5 +67,5 @@ class CollectXgen(pyblish.api.InstancePlugin): data["transfers"] = transfers - self.log.info(data) + self.log.debug(data) instance.data.update(data) diff --git a/openpype/hosts/maya/plugins/publish/collect_yeti_rig.py b/openpype/hosts/maya/plugins/publish/collect_yeti_rig.py index bc15edd9e0..df761cde13 100644 --- a/openpype/hosts/maya/plugins/publish/collect_yeti_rig.py +++ b/openpype/hosts/maya/plugins/publish/collect_yeti_rig.py @@ -119,7 +119,6 @@ class CollectYetiRig(pyblish.api.InstancePlugin): texture_filenames = [] if image_search_paths: - # TODO: Somehow this uses OS environment path separator, `:` vs `;` # Later on check whether this is pipeline OS cross-compatible. image_search_paths = [p for p in @@ -130,13 +129,13 @@ class CollectYetiRig(pyblish.api.InstancePlugin): # List all related textures texture_filenames = cmds.pgYetiCommand(node, listTextures=True) - self.log.info("Found %i texture(s)" % len(texture_filenames)) + self.log.debug("Found %i texture(s)" % len(texture_filenames)) # Get all reference nodes reference_nodes = cmds.pgYetiGraph(node, listNodes=True, type="reference") - self.log.info("Found %i reference node(s)" % len(reference_nodes)) + self.log.debug("Found %i reference node(s)" % len(reference_nodes)) if texture_filenames and not image_search_paths: raise ValueError("pgYetiMaya node '%s' is missing the path to the " diff --git a/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py b/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py index 102f0e46a2..46cc9090bb 100644 --- a/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/publish/extract_arnold_scene_source.py @@ -100,7 +100,7 @@ class ExtractArnoldSceneSource(publish.Extractor): instance.data["representations"].append(representation) - self.log.info( + self.log.debug( "Extracted instance {} to: {}".format(instance.name, staging_dir) ) @@ -126,7 +126,7 @@ class ExtractArnoldSceneSource(publish.Extractor): instance.data["representations"].append(representation) def _extract(self, nodes, attribute_data, kwargs): - self.log.info( + self.log.debug( "Writing {} with:\n{}".format(kwargs["filename"], kwargs) ) filenames = [] @@ -180,12 +180,12 @@ class ExtractArnoldSceneSource(publish.Extractor): with lib.attribute_values(attribute_data): with lib.maintained_selection(): - self.log.info( + self.log.debug( "Writing: {}".format(duplicate_nodes) ) cmds.select(duplicate_nodes, noExpand=True) - self.log.info( + self.log.debug( "Extracting ass sequence with: {}".format(kwargs) ) @@ -194,6 +194,6 @@ class ExtractArnoldSceneSource(publish.Extractor): for file in exported_files: filenames.append(os.path.split(file)[1]) - self.log.info("Exported: {}".format(filenames)) + self.log.debug("Exported: {}".format(filenames)) return filenames, nodes_by_id diff --git a/openpype/hosts/maya/plugins/publish/extract_assembly.py b/openpype/hosts/maya/plugins/publish/extract_assembly.py index 9b2978d192..86ffdcef24 100644 --- a/openpype/hosts/maya/plugins/publish/extract_assembly.py +++ b/openpype/hosts/maya/plugins/publish/extract_assembly.py @@ -27,7 +27,7 @@ class ExtractAssembly(publish.Extractor): json_filename = "{}.json".format(instance.name) json_path = os.path.join(staging_dir, json_filename) - self.log.info("Dumping scene data for debugging ..") + self.log.debug("Dumping scene data for debugging ..") with open(json_path, "w") as filepath: json.dump(instance.data["scenedata"], filepath, ensure_ascii=False) diff --git a/openpype/hosts/maya/plugins/publish/extract_camera_alembic.py b/openpype/hosts/maya/plugins/publish/extract_camera_alembic.py index aa445a0387..4ec1399df4 100644 --- a/openpype/hosts/maya/plugins/publish/extract_camera_alembic.py +++ b/openpype/hosts/maya/plugins/publish/extract_camera_alembic.py @@ -94,7 +94,7 @@ class ExtractCameraAlembic(publish.Extractor): "Attributes to bake must be specified as a list" ) for attr in self.bake_attributes: - self.log.info("Adding {} attribute".format(attr)) + self.log.debug("Adding {} attribute".format(attr)) job_str += " -attr {0}".format(attr) with lib.evaluation("off"): @@ -112,5 +112,5 @@ class ExtractCameraAlembic(publish.Extractor): } instance.data["representations"].append(representation) - self.log.info("Extracted instance '{0}' to: {1}".format( + self.log.debug("Extracted instance '{0}' to: {1}".format( instance.name, path)) diff --git a/openpype/hosts/maya/plugins/publish/extract_camera_mayaScene.py b/openpype/hosts/maya/plugins/publish/extract_camera_mayaScene.py index 30e6b89f2f..a50a8f0dfa 100644 --- a/openpype/hosts/maya/plugins/publish/extract_camera_mayaScene.py +++ b/openpype/hosts/maya/plugins/publish/extract_camera_mayaScene.py @@ -111,7 +111,7 @@ class ExtractCameraMayaScene(publish.Extractor): for family in self.families: try: self.scene_type = ext_mapping[family] - self.log.info( + self.log.debug( "Using {} as scene type".format(self.scene_type)) break except KeyError: @@ -151,7 +151,7 @@ class ExtractCameraMayaScene(publish.Extractor): with lib.evaluation("off"): with lib.suspended_refresh(): if bake_to_worldspace: - self.log.info( + self.log.debug( "Performing camera bakes: {}".format(transform)) baked = lib.bake_to_world_space( transform, @@ -186,7 +186,7 @@ class ExtractCameraMayaScene(publish.Extractor): unlock(plug) cmds.setAttr(plug, value) - self.log.info("Performing extraction..") + self.log.debug("Performing extraction..") cmds.select(cmds.ls(members, dag=True, shapes=True, long=True), noExpand=True) cmds.file(path, @@ -217,5 +217,5 @@ class ExtractCameraMayaScene(publish.Extractor): } instance.data["representations"].append(representation) - self.log.info("Extracted instance '{0}' to: {1}".format( + self.log.debug("Extracted instance '{0}' to: {1}".format( instance.name, path)) diff --git a/openpype/hosts/maya/plugins/publish/extract_fbx.py b/openpype/hosts/maya/plugins/publish/extract_fbx.py index 9af3acef65..4f7eaf57bf 100644 --- a/openpype/hosts/maya/plugins/publish/extract_fbx.py +++ b/openpype/hosts/maya/plugins/publish/extract_fbx.py @@ -33,11 +33,11 @@ class ExtractFBX(publish.Extractor): # to format it into a string in a mel expression path = path.replace('\\', '/') - self.log.info("Extracting FBX to: {0}".format(path)) + self.log.debug("Extracting FBX to: {0}".format(path)) members = instance.data["setMembers"] - self.log.info("Members: {0}".format(members)) - self.log.info("Instance: {0}".format(instance[:])) + self.log.debug("Members: {0}".format(members)) + self.log.debug("Instance: {0}".format(instance[:])) fbx_exporter.set_options_from_instance(instance) @@ -58,4 +58,4 @@ class ExtractFBX(publish.Extractor): } instance.data["representations"].append(representation) - self.log.info("Extract FBX successful to: {0}".format(path)) + self.log.debug("Extract FBX successful to: {0}".format(path)) diff --git a/openpype/hosts/maya/plugins/publish/extract_gltf.py b/openpype/hosts/maya/plugins/publish/extract_gltf.py index ac258ffb3d..6d72d28525 100644 --- a/openpype/hosts/maya/plugins/publish/extract_gltf.py +++ b/openpype/hosts/maya/plugins/publish/extract_gltf.py @@ -20,14 +20,10 @@ class ExtractGLB(publish.Extractor): filename = "{0}.glb".format(instance.name) path = os.path.join(staging_dir, filename) - self.log.info("Extracting GLB to: {}".format(path)) - cmds.loadPlugin("maya2glTF", quiet=True) nodes = instance[:] - self.log.info("Instance: {0}".format(nodes)) - start_frame = instance.data('frameStart') or \ int(cmds.playbackOptions(query=True, animationStartTime=True))# noqa @@ -48,6 +44,7 @@ class ExtractGLB(publish.Extractor): "vno": True # visibleNodeOnly } + self.log.debug("Extracting GLB to: {}".format(path)) with lib.maintained_selection(): cmds.select(nodes, hi=True, noExpand=True) extract_gltf(staging_dir, @@ -65,4 +62,4 @@ class ExtractGLB(publish.Extractor): } instance.data["representations"].append(representation) - self.log.info("Extract GLB successful to: {0}".format(path)) + self.log.debug("Extract GLB successful to: {0}".format(path)) diff --git a/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py b/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py index 422f5ad019..16436c6fe4 100644 --- a/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py +++ b/openpype/hosts/maya/plugins/publish/extract_gpu_cache.py @@ -60,6 +60,6 @@ class ExtractGPUCache(publish.Extractor): instance.data["representations"].append(representation) - self.log.info( + self.log.debug( "Extracted instance {} to: {}".format(instance.name, staging_dir) ) diff --git a/openpype/hosts/maya/plugins/publish/extract_import_reference.py b/openpype/hosts/maya/plugins/publish/extract_import_reference.py index 8bb82be9b6..9d2ff1a3eb 100644 --- a/openpype/hosts/maya/plugins/publish/extract_import_reference.py +++ b/openpype/hosts/maya/plugins/publish/extract_import_reference.py @@ -46,7 +46,7 @@ class ExtractImportReference(publish.Extractor, for family in self.families: try: self.scene_type = ext_mapping[family] - self.log.info( + self.log.debug( "Using {} as scene type".format(self.scene_type)) break @@ -69,7 +69,7 @@ class ExtractImportReference(publish.Extractor, reference_path = os.path.join(dir_path, ref_scene_name) tmp_path = os.path.dirname(current_name) + "/" + ref_scene_name - self.log.info("Performing extraction..") + self.log.debug("Performing extraction..") # This generates script for mayapy to take care of reference # importing outside current session. It is passing current scene @@ -111,7 +111,7 @@ print("*** Done") # process until handles are closed by context manager. with tempfile.TemporaryDirectory() as tmp_dir_name: tmp_script_path = os.path.join(tmp_dir_name, "import_ref.py") - self.log.info("Using script file: {}".format(tmp_script_path)) + self.log.debug("Using script file: {}".format(tmp_script_path)) with open(tmp_script_path, "wt") as tmp: tmp.write(script) @@ -149,9 +149,9 @@ print("*** Done") "stagingDir": os.path.dirname(current_name), "outputName": "imported" } - self.log.info("%s" % ref_representation) + self.log.debug(ref_representation) instance.data["representations"].append(ref_representation) - self.log.info("Extracted instance '%s' to : '%s'" % (ref_scene_name, - reference_path)) + self.log.debug("Extracted instance '%s' to : '%s'" % (ref_scene_name, + reference_path)) diff --git a/openpype/hosts/maya/plugins/publish/extract_layout.py b/openpype/hosts/maya/plugins/publish/extract_layout.py index bf5b4fc0e7..75920b44a2 100644 --- a/openpype/hosts/maya/plugins/publish/extract_layout.py +++ b/openpype/hosts/maya/plugins/publish/extract_layout.py @@ -23,7 +23,7 @@ class ExtractLayout(publish.Extractor): stagingdir = self.staging_dir(instance) # Perform extraction - self.log.info("Performing extraction..") + self.log.debug("Performing extraction..") if "representations" not in instance.data: instance.data["representations"] = [] @@ -64,7 +64,7 @@ class ExtractLayout(publish.Extractor): fields=["parent", "context.family"] ) - self.log.info(representation) + self.log.debug(representation) version_id = representation.get("parent") family = representation.get("context").get("family") @@ -159,5 +159,5 @@ class ExtractLayout(publish.Extractor): } instance.data["representations"].append(json_representation) - self.log.info("Extracted instance '%s' to: %s", - instance.name, json_representation) + self.log.debug("Extracted instance '%s' to: %s", + instance.name, json_representation) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index b13568c781..3506027a1f 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -307,7 +307,7 @@ class MakeTX(TextureProcessor): render_colorspace = color_management["rendering_space"] - self.log.info("tx: converting colorspace {0} " + self.log.debug("tx: converting colorspace {0} " "-> {1}".format(colorspace, render_colorspace)) args.extend(["--colorconvert", colorspace, render_colorspace]) @@ -331,7 +331,7 @@ class MakeTX(TextureProcessor): if not os.path.exists(resources_dir): os.makedirs(resources_dir) - self.log.info("Generating .tx file for %s .." % source) + self.log.debug("Generating .tx file for %s .." % source) subprocess_args = maketx_args + [ "-v", # verbose @@ -421,7 +421,7 @@ class ExtractLook(publish.Extractor): for family in self.families: try: self.scene_type = ext_mapping[family] - self.log.info( + self.log.debug( "Using {} as scene type".format(self.scene_type)) break except KeyError: @@ -453,7 +453,7 @@ class ExtractLook(publish.Extractor): relationships = lookdata["relationships"] sets = list(relationships.keys()) if not sets: - self.log.info("No sets found for the look") + self.log.debug("No sets found for the look") return # Specify texture processing executables to activate @@ -485,7 +485,7 @@ class ExtractLook(publish.Extractor): remap = results["attrRemap"] # Extract in correct render layer - self.log.info("Extracting look maya scene file: {}".format(maya_path)) + self.log.debug("Extracting look maya scene file: {}".format(maya_path)) layer = instance.data.get("renderlayer", "defaultRenderLayer") with lib.renderlayer(layer): # TODO: Ensure membership edits don't become renderlayer overrides @@ -511,12 +511,12 @@ class ExtractLook(publish.Extractor): ) # Write the JSON data - self.log.info("Extract json..") data = { "attributes": lookdata["attributes"], "relationships": relationships } + self.log.debug("Extracting json file: {}".format(json_path)) with open(json_path, "w") as f: json.dump(data, f) @@ -557,8 +557,8 @@ class ExtractLook(publish.Extractor): # Source hash for the textures instance.data["sourceHashes"] = hashes - self.log.info("Extracted instance '%s' to: %s" % (instance.name, - maya_path)) + self.log.debug("Extracted instance '%s' to: %s" % (instance.name, + maya_path)) def _set_resource_result_colorspace(self, resource, colorspace): """Update resource resulting colorspace after texture processing""" @@ -589,14 +589,13 @@ class ExtractLook(publish.Extractor): resources = instance.data["resources"] color_management = lib.get_color_management_preferences() - # Temporary fix to NOT create hardlinks on windows machines - if platform.system().lower() == "windows": - self.log.info( + force_copy = instance.data.get("forceCopy", False) + if not force_copy and platform.system().lower() == "windows": + # Temporary fix to NOT create hardlinks on windows machines + self.log.warning( "Forcing copy instead of hardlink due to issues on Windows..." ) force_copy = True - else: - force_copy = instance.data.get("forceCopy", False) destinations_cache = {} @@ -671,11 +670,11 @@ class ExtractLook(publish.Extractor): destination = get_resource_destination_cached(source) if force_copy or texture_result.transfer_mode == COPY: transfers.append((source, destination)) - self.log.info('file will be copied {} -> {}'.format( + self.log.debug('file will be copied {} -> {}'.format( source, destination)) elif texture_result.transfer_mode == HARDLINK: hardlinks.append((source, destination)) - self.log.info('file will be hardlinked {} -> {}'.format( + self.log.debug('file will be hardlinked {} -> {}'.format( source, destination)) # Store the hashes from hash to destination to include in the @@ -707,7 +706,7 @@ class ExtractLook(publish.Extractor): color_space_attr = "{}.colorSpace".format(node) remap[color_space_attr] = resource["result_color_space"] - self.log.info("Finished remapping destinations ...") + self.log.debug("Finished remapping destinations ...") return { "fileTransfers": transfers, @@ -815,8 +814,8 @@ class ExtractLook(publish.Extractor): if not processed_result: raise RuntimeError("Texture Processor {} returned " "no result.".format(processor)) - self.log.info("Generated processed " - "texture: {}".format(processed_result.path)) + self.log.debug("Generated processed " + "texture: {}".format(processed_result.path)) # TODO: Currently all processors force copy instead of allowing # hardlinks using source hashes. This should be refactored @@ -827,7 +826,7 @@ class ExtractLook(publish.Extractor): if not force_copy: existing = self._get_existing_hashed_texture(filepath) if existing: - self.log.info("Found hash in database, preparing hardlink..") + self.log.debug("Found hash in database, preparing hardlink..") return TextureResult( path=filepath, file_hash=texture_hash, diff --git a/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py b/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py index d87d6c208a..ab170fe48c 100644 --- a/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py +++ b/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py @@ -34,7 +34,7 @@ class ExtractMayaSceneRaw(publish.Extractor): for family in self.families: try: self.scene_type = ext_mapping[family] - self.log.info( + self.log.debug( "Using {} as scene type".format(self.scene_type)) break except KeyError: @@ -63,7 +63,7 @@ class ExtractMayaSceneRaw(publish.Extractor): selection += self._get_loaded_containers(members) # Perform extraction - self.log.info("Performing extraction ...") + self.log.debug("Performing extraction ...") with maintained_selection(): cmds.select(selection, noExpand=True) cmds.file(path, @@ -87,7 +87,8 @@ class ExtractMayaSceneRaw(publish.Extractor): } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, path)) + self.log.debug("Extracted instance '%s' to: %s" % (instance.name, + path)) @staticmethod def _get_loaded_containers(members): diff --git a/openpype/hosts/maya/plugins/publish/extract_model.py b/openpype/hosts/maya/plugins/publish/extract_model.py index 5137dffd94..29c952ebbc 100644 --- a/openpype/hosts/maya/plugins/publish/extract_model.py +++ b/openpype/hosts/maya/plugins/publish/extract_model.py @@ -44,7 +44,7 @@ class ExtractModel(publish.Extractor, for family in self.families: try: self.scene_type = ext_mapping[family] - self.log.info( + self.log.debug( "Using {} as scene type".format(self.scene_type)) break except KeyError: @@ -56,7 +56,7 @@ class ExtractModel(publish.Extractor, path = os.path.join(stagingdir, filename) # Perform extraction - self.log.info("Performing extraction ...") + self.log.debug("Performing extraction ...") # Get only the shape contents we need in such a way that we avoid # taking along intermediateObjects @@ -102,4 +102,5 @@ class ExtractModel(publish.Extractor, } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, path)) + self.log.debug("Extracted instance '%s' to: %s" % (instance.name, + path)) diff --git a/openpype/hosts/maya/plugins/publish/extract_multiverse_look.py b/openpype/hosts/maya/plugins/publish/extract_multiverse_look.py index 6fe7cf0d55..c2bebeaee6 100644 --- a/openpype/hosts/maya/plugins/publish/extract_multiverse_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_multiverse_look.py @@ -101,10 +101,10 @@ class ExtractMultiverseLook(publish.Extractor): # Parse export options options = self.default_options - self.log.info("Export options: {0}".format(options)) + self.log.debug("Export options: {0}".format(options)) # Perform extraction - self.log.info("Performing extraction ...") + self.log.debug("Performing extraction ...") with maintained_selection(): members = instance.data("setMembers") @@ -114,7 +114,7 @@ class ExtractMultiverseLook(publish.Extractor): type="mvUsdCompoundShape", noIntermediate=True, long=True) - self.log.info('Collected object {}'.format(members)) + self.log.debug('Collected object {}'.format(members)) if len(members) > 1: self.log.error('More than one member: {}'.format(members)) @@ -153,5 +153,5 @@ class ExtractMultiverseLook(publish.Extractor): } instance.data["representations"].append(representation) - self.log.info("Extracted instance {} to {}".format( + self.log.debug("Extracted instance {} to {}".format( instance.name, file_path)) diff --git a/openpype/hosts/maya/plugins/publish/extract_multiverse_usd.py b/openpype/hosts/maya/plugins/publish/extract_multiverse_usd.py index 4399eacda1..17d5891e59 100644 --- a/openpype/hosts/maya/plugins/publish/extract_multiverse_usd.py +++ b/openpype/hosts/maya/plugins/publish/extract_multiverse_usd.py @@ -150,7 +150,6 @@ class ExtractMultiverseUsd(publish.Extractor): return options def get_default_options(self): - self.log.info("ExtractMultiverseUsd get_default_options") return self.default_options def filter_members(self, members): @@ -173,19 +172,19 @@ class ExtractMultiverseUsd(publish.Extractor): # Parse export options options = self.get_default_options() options = self.parse_overrides(instance, options) - self.log.info("Export options: {0}".format(options)) + self.log.debug("Export options: {0}".format(options)) # Perform extraction - self.log.info("Performing extraction ...") + self.log.debug("Performing extraction ...") with maintained_selection(): members = instance.data("setMembers") - self.log.info('Collected objects: {}'.format(members)) + self.log.debug('Collected objects: {}'.format(members)) members = self.filter_members(members) if not members: self.log.error('No members!') return - self.log.info(' - filtered: {}'.format(members)) + self.log.debug(' - filtered: {}'.format(members)) import multiverse @@ -229,7 +228,7 @@ class ExtractMultiverseUsd(publish.Extractor): self.log.debug(" - {}={}".format(key, value)) setattr(asset_write_opts, key, value) - self.log.info('WriteAsset: {} / {}'.format(file_path, members)) + self.log.debug('WriteAsset: {} / {}'.format(file_path, members)) multiverse.WriteAsset(file_path, members, asset_write_opts) if "representations" not in instance.data: @@ -243,7 +242,7 @@ class ExtractMultiverseUsd(publish.Extractor): } instance.data["representations"].append(representation) - self.log.info("Extracted instance {} to {}".format( + self.log.debug("Extracted instance {} to {}".format( instance.name, file_path)) diff --git a/openpype/hosts/maya/plugins/publish/extract_multiverse_usd_comp.py b/openpype/hosts/maya/plugins/publish/extract_multiverse_usd_comp.py index a62729c198..7966c4fa93 100644 --- a/openpype/hosts/maya/plugins/publish/extract_multiverse_usd_comp.py +++ b/openpype/hosts/maya/plugins/publish/extract_multiverse_usd_comp.py @@ -105,14 +105,14 @@ class ExtractMultiverseUsdComposition(publish.Extractor): # Parse export options options = self.default_options options = self.parse_overrides(instance, options) - self.log.info("Export options: {0}".format(options)) + self.log.debug("Export options: {0}".format(options)) # Perform extraction - self.log.info("Performing extraction ...") + self.log.debug("Performing extraction ...") with maintained_selection(): members = instance.data("setMembers") - self.log.info('Collected object {}'.format(members)) + self.log.debug('Collected object {}'.format(members)) import multiverse @@ -175,5 +175,5 @@ class ExtractMultiverseUsdComposition(publish.Extractor): } instance.data["representations"].append(representation) - self.log.info("Extracted instance {} to {}".format( - instance.name, file_path)) + self.log.debug("Extracted instance {} to {}".format(instance.name, + file_path)) diff --git a/openpype/hosts/maya/plugins/publish/extract_multiverse_usd_over.py b/openpype/hosts/maya/plugins/publish/extract_multiverse_usd_over.py index cf610ac6b4..e4a97db6e4 100644 --- a/openpype/hosts/maya/plugins/publish/extract_multiverse_usd_over.py +++ b/openpype/hosts/maya/plugins/publish/extract_multiverse_usd_over.py @@ -87,10 +87,10 @@ class ExtractMultiverseUsdOverride(publish.Extractor): # Parse export options options = self.default_options - self.log.info("Export options: {0}".format(options)) + self.log.debug("Export options: {0}".format(options)) # Perform extraction - self.log.info("Performing extraction ...") + self.log.debug("Performing extraction ...") with maintained_selection(): members = instance.data("setMembers") @@ -100,7 +100,7 @@ class ExtractMultiverseUsdOverride(publish.Extractor): type="mvUsdCompoundShape", noIntermediate=True, long=True) - self.log.info("Collected object {}".format(members)) + self.log.debug("Collected object {}".format(members)) # TODO: Deal with asset, composition, override with options. import multiverse @@ -153,5 +153,5 @@ class ExtractMultiverseUsdOverride(publish.Extractor): } instance.data["representations"].append(representation) - self.log.info("Extracted instance {} to {}".format( + self.log.debug("Extracted instance {} to {}".format( instance.name, file_path)) diff --git a/openpype/hosts/maya/plugins/publish/extract_obj.py b/openpype/hosts/maya/plugins/publish/extract_obj.py index 518b0f0ff8..ca94130d09 100644 --- a/openpype/hosts/maya/plugins/publish/extract_obj.py +++ b/openpype/hosts/maya/plugins/publish/extract_obj.py @@ -30,7 +30,7 @@ class ExtractObj(publish.Extractor): # The export requires forward slashes because we need to # format it into a string in a mel expression - self.log.info("Extracting OBJ to: {0}".format(path)) + self.log.debug("Extracting OBJ to: {0}".format(path)) members = instance.data("setMembers") members = cmds.ls(members, @@ -39,8 +39,8 @@ class ExtractObj(publish.Extractor): type=("mesh", "nurbsCurve"), noIntermediate=True, long=True) - self.log.info("Members: {0}".format(members)) - self.log.info("Instance: {0}".format(instance[:])) + self.log.debug("Members: {0}".format(members)) + self.log.debug("Instance: {0}".format(instance[:])) if not cmds.pluginInfo('objExport', query=True, loaded=True): cmds.loadPlugin('objExport') @@ -74,4 +74,4 @@ class ExtractObj(publish.Extractor): } instance.data["representations"].append(representation) - self.log.info("Extract OBJ successful to: {0}".format(path)) + self.log.debug("Extract OBJ successful to: {0}".format(path)) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 9580c13841..cfab239da3 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -48,7 +48,7 @@ class ExtractPlayblast(publish.Extractor): self.log.debug("playblast path {}".format(path)) def process(self, instance): - self.log.info("Extracting capture..") + self.log.debug("Extracting capture..") # get scene fps fps = instance.data.get("fps") or instance.context.data.get("fps") @@ -62,7 +62,7 @@ class ExtractPlayblast(publish.Extractor): if end is None: end = cmds.playbackOptions(query=True, animationEndTime=True) - self.log.info("start: {}, end: {}".format(start, end)) + self.log.debug("start: {}, end: {}".format(start, end)) # get cameras camera = instance.data["review_camera"] @@ -119,7 +119,7 @@ class ExtractPlayblast(publish.Extractor): filename = "{0}".format(instance.name) path = os.path.join(stagingdir, filename) - self.log.info("Outputting images to %s" % path) + self.log.debug("Outputting images to %s" % path) preset["filename"] = path preset["overwrite"] = True @@ -237,7 +237,7 @@ class ExtractPlayblast(publish.Extractor): self.log.debug("collection head {}".format(filebase)) if filebase in filename: frame_collection = collection - self.log.info( + self.log.debug( "we found collection of interest {}".format( str(frame_collection))) diff --git a/openpype/hosts/maya/plugins/publish/extract_pointcache.py b/openpype/hosts/maya/plugins/publish/extract_pointcache.py index 9537a11ee4..5530446e3d 100644 --- a/openpype/hosts/maya/plugins/publish/extract_pointcache.py +++ b/openpype/hosts/maya/plugins/publish/extract_pointcache.py @@ -109,11 +109,11 @@ class ExtractAlembic(publish.Extractor): instance.context.data["cleanupFullPaths"].append(path) - self.log.info("Extracted {} to {}".format(instance, dirname)) + self.log.debug("Extracted {} to {}".format(instance, dirname)) # Extract proxy. if not instance.data.get("proxy"): - self.log.info("No proxy nodes found. Skipping proxy extraction.") + self.log.debug("No proxy nodes found. Skipping proxy extraction.") return path = path.replace(".abc", "_proxy.abc") diff --git a/openpype/hosts/maya/plugins/publish/extract_proxy_abc.py b/openpype/hosts/maya/plugins/publish/extract_proxy_abc.py index cf6351fdca..921ee44a24 100644 --- a/openpype/hosts/maya/plugins/publish/extract_proxy_abc.py +++ b/openpype/hosts/maya/plugins/publish/extract_proxy_abc.py @@ -32,7 +32,7 @@ class ExtractProxyAlembic(publish.Extractor): attr_prefixes = instance.data.get("attrPrefix", "").split(";") attr_prefixes = [value for value in attr_prefixes if value.strip()] - self.log.info("Extracting Proxy Alembic..") + self.log.debug("Extracting Proxy Alembic..") dirname = self.staging_dir(instance) filename = "{name}.abc".format(**instance.data) @@ -82,7 +82,7 @@ class ExtractProxyAlembic(publish.Extractor): instance.context.data["cleanupFullPaths"].append(path) - self.log.info("Extracted {} to {}".format(instance, dirname)) + self.log.debug("Extracted {} to {}".format(instance, dirname)) # remove the bounding box bbox_master = cmds.ls("bbox_grp") cmds.delete(bbox_master) diff --git a/openpype/hosts/maya/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/maya/plugins/publish/extract_redshift_proxy.py index 834b335fc5..3868270b79 100644 --- a/openpype/hosts/maya/plugins/publish/extract_redshift_proxy.py +++ b/openpype/hosts/maya/plugins/publish/extract_redshift_proxy.py @@ -59,7 +59,7 @@ class ExtractRedshiftProxy(publish.Extractor): # vertex_colors = instance.data.get("vertexColors", False) # Write out rs file - self.log.info("Writing: '%s'" % file_path) + self.log.debug("Writing: '%s'" % file_path) with maintained_selection(): cmds.select(instance.data["setMembers"], noExpand=True) cmds.file(file_path, @@ -82,5 +82,5 @@ class ExtractRedshiftProxy(publish.Extractor): } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" - % (instance.name, staging_dir)) + self.log.debug("Extracted instance '%s' to: %s" + % (instance.name, staging_dir)) diff --git a/openpype/hosts/maya/plugins/publish/extract_rendersetup.py b/openpype/hosts/maya/plugins/publish/extract_rendersetup.py index 5970c038a4..7e21f5282e 100644 --- a/openpype/hosts/maya/plugins/publish/extract_rendersetup.py +++ b/openpype/hosts/maya/plugins/publish/extract_rendersetup.py @@ -37,5 +37,5 @@ class ExtractRenderSetup(publish.Extractor): } instance.data["representations"].append(representation) - self.log.info( + self.log.debug( "Extracted instance '%s' to: %s" % (instance.name, json_path)) diff --git a/openpype/hosts/maya/plugins/publish/extract_rig.py b/openpype/hosts/maya/plugins/publish/extract_rig.py index be57b9de07..1ffc9a7dae 100644 --- a/openpype/hosts/maya/plugins/publish/extract_rig.py +++ b/openpype/hosts/maya/plugins/publish/extract_rig.py @@ -27,7 +27,7 @@ class ExtractRig(publish.Extractor): for family in self.families: try: self.scene_type = ext_mapping[family] - self.log.info( + self.log.debug( "Using '.{}' as scene type".format(self.scene_type)) break except AttributeError: @@ -39,7 +39,7 @@ class ExtractRig(publish.Extractor): path = os.path.join(dir_path, filename) # Perform extraction - self.log.info("Performing extraction ...") + self.log.debug("Performing extraction ...") with maintained_selection(): cmds.select(instance, noExpand=True) cmds.file(path, @@ -63,4 +63,4 @@ class ExtractRig(publish.Extractor): } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, path)) + self.log.debug("Extracted instance '%s' to: %s", instance.name, path) diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index 4160ac4cb2..e44204cae0 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -24,7 +24,7 @@ class ExtractThumbnail(publish.Extractor): families = ["review"] def process(self, instance): - self.log.info("Extracting capture..") + self.log.debug("Extracting capture..") camera = instance.data["review_camera"] @@ -96,7 +96,7 @@ class ExtractThumbnail(publish.Extractor): filename = "{0}".format(instance.name) path = os.path.join(dst_staging, filename) - self.log.info("Outputting images to %s" % path) + self.log.debug("Outputting images to %s" % path) preset["filename"] = path preset["overwrite"] = True @@ -159,7 +159,7 @@ class ExtractThumbnail(publish.Extractor): _, thumbnail = os.path.split(playblast) - self.log.info("file list {}".format(thumbnail)) + self.log.debug("file list {}".format(thumbnail)) if "representations" not in instance.data: instance.data["representations"] = [] diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh_abc.py b/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh_abc.py index 4a797eb462..9c2f55a1ef 100644 --- a/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh_abc.py +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh_abc.py @@ -57,9 +57,9 @@ class ExtractUnrealSkeletalMeshAbc(publish.Extractor): # to format it into a string in a mel expression path = path.replace('\\', '/') - self.log.info("Extracting ABC to: {0}".format(path)) - self.log.info("Members: {0}".format(nodes)) - self.log.info("Instance: {0}".format(instance[:])) + self.log.debug("Extracting ABC to: {0}".format(path)) + self.log.debug("Members: {0}".format(nodes)) + self.log.debug("Instance: {0}".format(instance[:])) options = { "step": instance.data.get("step", 1.0), @@ -74,7 +74,7 @@ class ExtractUnrealSkeletalMeshAbc(publish.Extractor): "worldSpace": instance.data.get("worldSpace", True) } - self.log.info("Options: {}".format(options)) + self.log.debug("Options: {}".format(options)) if int(cmds.about(version=True)) >= 2017: # Since Maya 2017 alembic supports multiple uv sets - write them. @@ -105,4 +105,4 @@ class ExtractUnrealSkeletalMeshAbc(publish.Extractor): } instance.data["representations"].append(representation) - self.log.info("Extract ABC successful to: {0}".format(path)) + self.log.debug("Extract ABC successful to: {0}".format(path)) diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh_fbx.py b/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh_fbx.py index b162ce47f7..96175a07d7 100644 --- a/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh_fbx.py +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_skeletalmesh_fbx.py @@ -46,9 +46,9 @@ class ExtractUnrealSkeletalMeshFbx(publish.Extractor): # to format it into a string in a mel expression path = path.replace('\\', '/') - self.log.info("Extracting FBX to: {0}".format(path)) - self.log.info("Members: {0}".format(to_extract)) - self.log.info("Instance: {0}".format(instance[:])) + self.log.debug("Extracting FBX to: {0}".format(path)) + self.log.debug("Members: {0}".format(to_extract)) + self.log.debug("Instance: {0}".format(instance[:])) fbx_exporter.set_options_from_instance(instance) @@ -70,7 +70,7 @@ class ExtractUnrealSkeletalMeshFbx(publish.Extractor): renamed_to_extract.append("|".join(node_path)) with renamed(original_parent, parent_node): - self.log.info("Extracting: {}".format(renamed_to_extract, path)) + self.log.debug("Extracting: {}".format(renamed_to_extract, path)) fbx_exporter.export(renamed_to_extract, path) if "representations" not in instance.data: @@ -84,4 +84,4 @@ class ExtractUnrealSkeletalMeshFbx(publish.Extractor): } instance.data["representations"].append(representation) - self.log.info("Extract FBX successful to: {0}".format(path)) + self.log.debug("Extract FBX successful to: {0}".format(path)) diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py index 44f0615a27..26ab0827e4 100644 --- a/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_staticmesh.py @@ -37,15 +37,15 @@ class ExtractUnrealStaticMesh(publish.Extractor): # to format it into a string in a mel expression path = path.replace('\\', '/') - self.log.info("Extracting FBX to: {0}".format(path)) - self.log.info("Members: {0}".format(members)) - self.log.info("Instance: {0}".format(instance[:])) + self.log.debug("Extracting FBX to: {0}".format(path)) + self.log.debug("Members: {0}".format(members)) + self.log.debug("Instance: {0}".format(instance[:])) fbx_exporter.set_options_from_instance(instance) with maintained_selection(): with parent_nodes(members): - self.log.info("Un-parenting: {}".format(members)) + self.log.debug("Un-parenting: {}".format(members)) fbx_exporter.export(members, path) if "representations" not in instance.data: @@ -59,4 +59,4 @@ class ExtractUnrealStaticMesh(publish.Extractor): } instance.data["representations"].append(representation) - self.log.info("Extract FBX successful to: {0}".format(path)) + self.log.debug("Extract FBX successful to: {0}".format(path)) diff --git a/openpype/hosts/maya/plugins/publish/extract_vrayproxy.py b/openpype/hosts/maya/plugins/publish/extract_vrayproxy.py index df16c6c357..21dfcfffc5 100644 --- a/openpype/hosts/maya/plugins/publish/extract_vrayproxy.py +++ b/openpype/hosts/maya/plugins/publish/extract_vrayproxy.py @@ -43,7 +43,7 @@ class ExtractVRayProxy(publish.Extractor): vertex_colors = instance.data.get("vertexColors", False) # Write out vrmesh file - self.log.info("Writing: '%s'" % file_path) + self.log.debug("Writing: '%s'" % file_path) with maintained_selection(): cmds.select(instance.data["setMembers"], noExpand=True) cmds.vrayCreateProxy(exportType=1, @@ -68,5 +68,5 @@ class ExtractVRayProxy(publish.Extractor): } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" - % (instance.name, staging_dir)) + self.log.debug("Extracted instance '%s' to: %s" + % (instance.name, staging_dir)) diff --git a/openpype/hosts/maya/plugins/publish/extract_vrayscene.py b/openpype/hosts/maya/plugins/publish/extract_vrayscene.py index 8442df1611..b0615149a9 100644 --- a/openpype/hosts/maya/plugins/publish/extract_vrayscene.py +++ b/openpype/hosts/maya/plugins/publish/extract_vrayscene.py @@ -20,13 +20,13 @@ class ExtractVrayscene(publish.Extractor): def process(self, instance): """Plugin entry point.""" if instance.data.get("exportOnFarm"): - self.log.info("vrayscenes will be exported on farm.") + self.log.debug("vrayscenes will be exported on farm.") raise NotImplementedError( "exporting vrayscenes is not implemented") # handle sequence if instance.data.get("vraySceneMultipleFiles"): - self.log.info("vrayscenes will be exported on farm.") + self.log.debug("vrayscenes will be exported on farm.") raise NotImplementedError( "exporting vrayscene sequences not implemented yet") @@ -40,7 +40,6 @@ class ExtractVrayscene(publish.Extractor): layer_name = instance.data.get("layer") staging_dir = self.staging_dir(instance) - self.log.info("staging: {}".format(staging_dir)) template = cmds.getAttr("{}.vrscene_filename".format(node)) start_frame = instance.data.get( "frameStartHandle") if instance.data.get( @@ -56,21 +55,21 @@ class ExtractVrayscene(publish.Extractor): staging_dir, "vrayscene", *formatted_name.split("/")) # Write out vrscene file - self.log.info("Writing: '%s'" % file_path) + self.log.debug("Writing: '%s'" % file_path) with maintained_selection(): if "*" not in instance.data["setMembers"]: - self.log.info( + self.log.debug( "Exporting: {}".format(instance.data["setMembers"])) set_members = instance.data["setMembers"] cmds.select(set_members, noExpand=True) else: - self.log.info("Exporting all ...") + self.log.debug("Exporting all ...") set_members = cmds.ls( long=True, objectsOnly=True, geometry=True, lights=True, cameras=True) cmds.select(set_members, noExpand=True) - self.log.info("Appending layer name {}".format(layer_name)) + self.log.debug("Appending layer name {}".format(layer_name)) set_members.append(layer_name) export_in_rs_layer( @@ -93,8 +92,8 @@ class ExtractVrayscene(publish.Extractor): } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" - % (instance.name, staging_dir)) + self.log.debug("Extracted instance '%s' to: %s" + % (instance.name, staging_dir)) @staticmethod def format_vray_output_filename( diff --git a/openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py b/openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py index 0d2a97bc4b..4bd01c2df2 100644 --- a/openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py +++ b/openpype/hosts/maya/plugins/publish/extract_workfile_xgen.py @@ -241,7 +241,7 @@ class ExtractWorkfileXgen(publish.Extractor): data[palette] = {attr: old_value} cmds.setAttr(node_attr, value, type="string") - self.log.info( + self.log.debug( "Setting \"{}\" on \"{}\"".format(value, node_attr) ) diff --git a/openpype/hosts/maya/plugins/publish/extract_xgen.py b/openpype/hosts/maya/plugins/publish/extract_xgen.py index 3c9d0bd344..8409330e49 100644 --- a/openpype/hosts/maya/plugins/publish/extract_xgen.py +++ b/openpype/hosts/maya/plugins/publish/extract_xgen.py @@ -77,7 +77,7 @@ class ExtractXgen(publish.Extractor): xgenm.exportPalette( instance.data["xgmPalette"].replace("|", ""), temp_xgen_path ) - self.log.info("Extracted to {}".format(temp_xgen_path)) + self.log.debug("Extracted to {}".format(temp_xgen_path)) # Import xgen onto the duplicate. with maintained_selection(): @@ -118,7 +118,7 @@ class ExtractXgen(publish.Extractor): expressions=True ) - self.log.info("Extracted to {}".format(maya_filepath)) + self.log.debug("Extracted to {}".format(maya_filepath)) if os.path.exists(temp_xgen_path): os.remove(temp_xgen_path) diff --git a/openpype/hosts/maya/plugins/publish/extract_yeti_cache.py b/openpype/hosts/maya/plugins/publish/extract_yeti_cache.py index b61f599cab..b113e02219 100644 --- a/openpype/hosts/maya/plugins/publish/extract_yeti_cache.py +++ b/openpype/hosts/maya/plugins/publish/extract_yeti_cache.py @@ -39,7 +39,7 @@ class ExtractYetiCache(publish.Extractor): else: kwargs.update({"samples": samples}) - self.log.info( + self.log.debug( "Writing out cache {} - {}".format(start_frame, end_frame)) # Start writing the files for snap shot # will be replace by the Yeti node name @@ -53,7 +53,7 @@ class ExtractYetiCache(publish.Extractor): cache_files = [x for x in os.listdir(dirname) if x.endswith(".fur")] - self.log.info("Writing metadata file") + self.log.debug("Writing metadata file") settings = instance.data["fursettings"] fursettings_path = os.path.join(dirname, "yeti.fursettings") with open(fursettings_path, "w") as fp: @@ -63,7 +63,7 @@ class ExtractYetiCache(publish.Extractor): if "representations" not in instance.data: instance.data["representations"] = [] - self.log.info("cache files: {}".format(cache_files[0])) + self.log.debug("cache files: {}".format(cache_files[0])) # Workaround: We do not explicitly register these files with the # representation solely so that we can write multiple sequences @@ -87,4 +87,4 @@ class ExtractYetiCache(publish.Extractor): } ) - self.log.info("Extracted {} to {}".format(instance, dirname)) + self.log.debug("Extracted {} to {}".format(instance, dirname)) diff --git a/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py b/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py index 9a46c31177..da67cb911f 100644 --- a/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py +++ b/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py @@ -109,7 +109,7 @@ class ExtractYetiRig(publish.Extractor): for family in self.families: try: self.scene_type = ext_mapping[family] - self.log.info( + self.log.debug( "Using {} as scene type".format(self.scene_type)) break except KeyError: @@ -127,7 +127,7 @@ class ExtractYetiRig(publish.Extractor): maya_path = os.path.join(dirname, "yeti_rig.{}".format(self.scene_type)) - self.log.info("Writing metadata file") + self.log.debug("Writing metadata file: {}".format(settings_path)) image_search_path = resources_dir = instance.data["resourcesDir"] @@ -147,7 +147,7 @@ class ExtractYetiRig(publish.Extractor): dst = os.path.join(image_search_path, os.path.basename(file)) instance.data['transfers'].append([src, dst]) - self.log.info("adding transfer {} -> {}". format(src, dst)) + self.log.debug("adding transfer {} -> {}". format(src, dst)) # Ensure the imageSearchPath is being remapped to the publish folder attr_value = {"%s.imageSearchPath" % n: str(image_search_path) for @@ -182,7 +182,7 @@ class ExtractYetiRig(publish.Extractor): if "representations" not in instance.data: instance.data["representations"] = [] - self.log.info("rig file: {}".format(maya_path)) + self.log.debug("rig file: {}".format(maya_path)) instance.data["representations"].append( { 'name': self.scene_type, @@ -191,7 +191,7 @@ class ExtractYetiRig(publish.Extractor): 'stagingDir': dirname } ) - self.log.info("settings file: {}".format(settings_path)) + self.log.debug("settings file: {}".format(settings_path)) instance.data["representations"].append( { 'name': 'rigsettings', @@ -201,6 +201,6 @@ class ExtractYetiRig(publish.Extractor): } ) - self.log.info("Extracted {} to {}".format(instance, dirname)) + self.log.debug("Extracted {} to {}".format(instance, dirname)) cmds.select(clear=True) diff --git a/openpype/hosts/maya/plugins/publish/reset_xgen_attributes.py b/openpype/hosts/maya/plugins/publish/reset_xgen_attributes.py index d8e8554b68..759aa23258 100644 --- a/openpype/hosts/maya/plugins/publish/reset_xgen_attributes.py +++ b/openpype/hosts/maya/plugins/publish/reset_xgen_attributes.py @@ -23,7 +23,7 @@ class ResetXgenAttributes(pyblish.api.InstancePlugin): for palette, data in xgen_attributes.items(): for attr, value in data.items(): node_attr = "{}.{}".format(palette, attr) - self.log.info( + self.log.debug( "Setting \"{}\" on \"{}\"".format(value, node_attr) ) cmds.setAttr(node_attr, value, type="string") @@ -32,5 +32,5 @@ class ResetXgenAttributes(pyblish.api.InstancePlugin): # Need to save the scene, cause the attribute changes above does not # mark the scene as modified so user can exit without committing the # changes. - self.log.info("Saving changes.") + self.log.debug("Saving changes.") cmds.file(save=True) diff --git a/openpype/hosts/maya/plugins/publish/submit_maya_muster.py b/openpype/hosts/maya/plugins/publish/submit_maya_muster.py index 8e219eae85..ef58140115 100644 --- a/openpype/hosts/maya/plugins/publish/submit_maya_muster.py +++ b/openpype/hosts/maya/plugins/publish/submit_maya_muster.py @@ -215,9 +215,9 @@ class MayaSubmitMuster(pyblish.api.InstancePlugin): :rtype: int :raises: Exception if template ID isn't found """ - self.log.info("Trying to find template for [{}]".format(renderer)) + self.log.debug("Trying to find template for [{}]".format(renderer)) mapped = _get_template_id(renderer) - self.log.info("got id [{}]".format(mapped)) + self.log.debug("got id [{}]".format(mapped)) return self._templates.get(mapped) def _submit(self, payload): @@ -454,8 +454,8 @@ class MayaSubmitMuster(pyblish.api.InstancePlugin): self.preflight_check(instance) - self.log.info("Submitting ...") - self.log.info(json.dumps(payload, indent=4, sort_keys=True)) + self.log.debug("Submitting ...") + self.log.debug(json.dumps(payload, indent=4, sort_keys=True)) response = self._submit(payload) # response = requests.post(url, json=payload) diff --git a/openpype/hosts/maya/plugins/publish/validate_assembly_name.py b/openpype/hosts/maya/plugins/publish/validate_assembly_name.py index bcc40760e0..00588cd300 100644 --- a/openpype/hosts/maya/plugins/publish/validate_assembly_name.py +++ b/openpype/hosts/maya/plugins/publish/validate_assembly_name.py @@ -20,7 +20,7 @@ class ValidateAssemblyName(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance): - cls.log.info("Checking name of {}".format(instance.name)) + cls.log.debug("Checking name of {}".format(instance.name)) content_instance = instance.data.get("setMembers", None) if not content_instance: diff --git a/openpype/hosts/maya/plugins/publish/validate_assembly_namespaces.py b/openpype/hosts/maya/plugins/publish/validate_assembly_namespaces.py index 41ef78aab4..06577f38f7 100644 --- a/openpype/hosts/maya/plugins/publish/validate_assembly_namespaces.py +++ b/openpype/hosts/maya/plugins/publish/validate_assembly_namespaces.py @@ -23,7 +23,7 @@ class ValidateAssemblyNamespaces(pyblish.api.InstancePlugin): def process(self, instance): - self.log.info("Checking namespace for %s" % instance.name) + self.log.debug("Checking namespace for %s" % instance.name) if self.get_invalid(instance): raise PublishValidationError("Nested namespaces found") diff --git a/openpype/hosts/maya/plugins/publish/validate_frame_range.py b/openpype/hosts/maya/plugins/publish/validate_frame_range.py index c6184ed348..a7043b8407 100644 --- a/openpype/hosts/maya/plugins/publish/validate_frame_range.py +++ b/openpype/hosts/maya/plugins/publish/validate_frame_range.py @@ -47,10 +47,10 @@ class ValidateFrameRange(pyblish.api.InstancePlugin, context = instance.context if instance.data.get("tileRendering"): - self.log.info(( + self.log.debug( "Skipping frame range validation because " "tile rendering is enabled." - )) + ) return frame_start_handle = int(context.data.get("frameStartHandle")) diff --git a/openpype/hosts/maya/plugins/publish/validate_glsl_material.py b/openpype/hosts/maya/plugins/publish/validate_glsl_material.py index 10c48da404..3b386c3def 100644 --- a/openpype/hosts/maya/plugins/publish/validate_glsl_material.py +++ b/openpype/hosts/maya/plugins/publish/validate_glsl_material.py @@ -75,7 +75,7 @@ class ValidateGLSLMaterial(pyblish.api.InstancePlugin): """ meshes = cmds.ls(instance, type="mesh", long=True) - cls.log.info("meshes: {}".format(meshes)) + cls.log.debug("meshes: {}".format(meshes)) # load the glsl shader plugin cmds.loadPlugin("glslShader", quiet=True) @@ -96,8 +96,8 @@ class ValidateGLSLMaterial(pyblish.api.InstancePlugin): cls.log.warning("ogsfx shader file " "not found in {}".format(ogsfx_path)) - cls.log.info("Find the ogsfx shader file in " - "default maya directory...") + cls.log.debug("Searching the ogsfx shader file in " + "default maya directory...") # re-direct to search the ogsfx path in maya_dir ogsfx_path = os.getenv("MAYA_APP_DIR") + ogsfx_path if not os.path.exists(ogsfx_path): @@ -130,8 +130,8 @@ class ValidateGLSLMaterial(pyblish.api.InstancePlugin): @classmethod def pbs_shader_conversion(cls, main_shader, glsl): - cls.log.info("StringrayPBS detected " - "-> Can do texture conversion") + cls.log.debug("StringrayPBS detected " + "-> Can do texture conversion") for shader in main_shader: # get the file textures related to the PBS Shader @@ -168,8 +168,8 @@ class ValidateGLSLMaterial(pyblish.api.InstancePlugin): @classmethod def arnold_shader_conversion(cls, main_shader, glsl): - cls.log.info("aiStandardSurface detected " - "-> Can do texture conversion") + cls.log.debug("aiStandardSurface detected " + "-> Can do texture conversion") for shader in main_shader: # get the file textures related to the PBS Shader diff --git a/openpype/hosts/maya/plugins/publish/validate_instancer_content.py b/openpype/hosts/maya/plugins/publish/validate_instancer_content.py index 2f14693ef2..236adfb03d 100644 --- a/openpype/hosts/maya/plugins/publish/validate_instancer_content.py +++ b/openpype/hosts/maya/plugins/publish/validate_instancer_content.py @@ -21,7 +21,7 @@ class ValidateInstancerContent(pyblish.api.InstancePlugin): members = instance.data['setMembers'] export_members = instance.data['exactExportMembers'] - self.log.info("Contents {0}".format(members)) + self.log.debug("Contents {0}".format(members)) if not len(members) == len(cmds.ls(members, type="instancer")): self.log.error("Instancer can only contain instancers") diff --git a/openpype/hosts/maya/plugins/publish/validate_instancer_frame_ranges.py b/openpype/hosts/maya/plugins/publish/validate_instancer_frame_ranges.py index fcfcdce8b6..714c6229d6 100644 --- a/openpype/hosts/maya/plugins/publish/validate_instancer_frame_ranges.py +++ b/openpype/hosts/maya/plugins/publish/validate_instancer_frame_ranges.py @@ -5,8 +5,6 @@ import pyblish.api from openpype.pipeline.publish import PublishValidationError -VERBOSE = False - def is_cache_resource(resource): """Return whether resource is a cacheFile resource""" @@ -73,9 +71,6 @@ class ValidateInstancerFrameRanges(pyblish.api.InstancePlugin): xml = all_files.pop(0) assert xml.endswith(".xml") - if VERBOSE: - cls.log.info("Checking: {0}".format(all_files)) - # Ensure all files exist (including ticks) # The remainder file paths should be the .mcx or .mcc files valdidate_files(all_files) @@ -129,8 +124,8 @@ class ValidateInstancerFrameRanges(pyblish.api.InstancePlugin): # for the frames required by the time range. if ticks: ticks = list(sorted(ticks)) - cls.log.info("Found ticks: {0} " - "(substeps: {1})".format(ticks, len(ticks))) + cls.log.debug("Found ticks: {0} " + "(substeps: {1})".format(ticks, len(ticks))) # Check all frames except the last since we don't # require subframes after our time range. diff --git a/openpype/hosts/maya/plugins/publish/validate_model_name.py b/openpype/hosts/maya/plugins/publish/validate_model_name.py index 6948dcf724..f4c1aa39c7 100644 --- a/openpype/hosts/maya/plugins/publish/validate_model_name.py +++ b/openpype/hosts/maya/plugins/publish/validate_model_name.py @@ -125,7 +125,7 @@ class ValidateModelName(pyblish.api.InstancePlugin, r = re.compile(regex) for obj in filtered: - cls.log.info("testing: {}".format(obj)) + cls.log.debug("testing: {}".format(obj)) m = r.match(obj) if m is None: cls.log.error("invalid name on: {}".format(obj)) diff --git a/openpype/hosts/maya/plugins/publish/validate_mvlook_contents.py b/openpype/hosts/maya/plugins/publish/validate_mvlook_contents.py index 68784a165d..ad0fcafc56 100644 --- a/openpype/hosts/maya/plugins/publish/validate_mvlook_contents.py +++ b/openpype/hosts/maya/plugins/publish/validate_mvlook_contents.py @@ -35,12 +35,12 @@ class ValidateMvLookContents(pyblish.api.InstancePlugin, publishMipMap = instance.data["publishMipMap"] enforced = True if intent in self.enforced_intents: - self.log.info("This validation will be enforced: '{}'" - .format(intent)) + self.log.debug("This validation will be enforced: '{}'" + .format(intent)) else: enforced = False - self.log.info("This validation will NOT be enforced: '{}'" - .format(intent)) + self.log.debug("This validation will NOT be enforced: '{}'" + .format(intent)) if not instance[:]: raise PublishValidationError("Instance is empty") @@ -75,8 +75,9 @@ class ValidateMvLookContents(pyblish.api.InstancePlugin, self.log.warning(msg) if invalid: - raise PublishValidationError("'{}' has invalid look " - "content".format(instance.name)) + raise PublishValidationError( + "'{}' has invalid look content".format(instance.name) + ) def valid_file(self, fname): self.log.debug("Checking validity of '{}'".format(fname)) diff --git a/openpype/hosts/maya/plugins/publish/validate_skeletalmesh_hierarchy.py b/openpype/hosts/maya/plugins/publish/validate_skeletalmesh_hierarchy.py index 398b6fb7bf..9084374c76 100644 --- a/openpype/hosts/maya/plugins/publish/validate_skeletalmesh_hierarchy.py +++ b/openpype/hosts/maya/plugins/publish/validate_skeletalmesh_hierarchy.py @@ -28,7 +28,7 @@ class ValidateSkeletalMeshHierarchy(pyblish.api.InstancePlugin): parent.split("|")[1] for parent in (joints_parents + geo_parents) } - self.log.info(parents_set) + self.log.debug(parents_set) if len(set(parents_set)) > 2: raise PublishXmlValidationError( diff --git a/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py b/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py index b2cb2ebda2..5ba256f9f5 100644 --- a/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py +++ b/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py @@ -140,12 +140,12 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin, return if not self.validate_mesh and not self.validate_collision: - self.log.info("Validation of both mesh and collision names" - "is disabled.") + self.log.debug("Validation of both mesh and collision names" + "is disabled.") return if not instance.data.get("collisionMembers", None): - self.log.info("There are no collision objects to validate") + self.log.debug("There are no collision objects to validate") return invalid = self.get_invalid(instance) diff --git a/openpype/hosts/maya/plugins/publish/validate_vray_distributed_rendering.py b/openpype/hosts/maya/plugins/publish/validate_vray_distributed_rendering.py index a71849da00..14571203ea 100644 --- a/openpype/hosts/maya/plugins/publish/validate_vray_distributed_rendering.py +++ b/openpype/hosts/maya/plugins/publish/validate_vray_distributed_rendering.py @@ -52,6 +52,6 @@ class ValidateVRayDistributedRendering(pyblish.api.InstancePlugin): renderlayer = instance.data.get("renderlayer") with lib.renderlayer(renderlayer): - cls.log.info("Enabling Distributed Rendering " - "ignore in batch mode..") + cls.log.debug("Enabling Distributed Rendering " + "ignore in batch mode..") cmds.setAttr(cls.ignored_attr, True) diff --git a/openpype/hosts/maya/plugins/publish/validate_yeti_renderscript_callbacks.py b/openpype/hosts/maya/plugins/publish/validate_yeti_renderscript_callbacks.py index 06250f5779..a8085418e7 100644 --- a/openpype/hosts/maya/plugins/publish/validate_yeti_renderscript_callbacks.py +++ b/openpype/hosts/maya/plugins/publish/validate_yeti_renderscript_callbacks.py @@ -54,7 +54,7 @@ class ValidateYetiRenderScriptCallbacks(pyblish.api.InstancePlugin): # has any yeti callback set or not since if the callback # is there it wouldn't error and if it weren't then # nothing happens because there are no yeti nodes. - cls.log.info( + cls.log.debug( "Yeti is loaded but no yeti nodes were found. " "Callback validation skipped.." ) @@ -62,7 +62,7 @@ class ValidateYetiRenderScriptCallbacks(pyblish.api.InstancePlugin): renderer = instance.data["renderer"] if renderer == "redshift": - cls.log.info("Redshift ignores any pre and post render callbacks") + cls.log.debug("Redshift ignores any pre and post render callbacks") return False callback_lookup = cls.callbacks.get(renderer, {}) diff --git a/openpype/hosts/maya/plugins/publish/validate_yeti_rig_input_in_instance.py b/openpype/hosts/maya/plugins/publish/validate_yeti_rig_input_in_instance.py index 96fb475a0a..50a27589ad 100644 --- a/openpype/hosts/maya/plugins/publish/validate_yeti_rig_input_in_instance.py +++ b/openpype/hosts/maya/plugins/publish/validate_yeti_rig_input_in_instance.py @@ -37,8 +37,8 @@ class ValidateYetiRigInputShapesInInstance(pyblish.api.Validator): # Allow publish without input meshes. if not shapes: - cls.log.info("Found no input meshes for %s, skipping ..." - % instance) + cls.log.debug("Found no input meshes for %s, skipping ..." + % instance) return [] # check if input node is part of groomRig instance From eec1d82db2b605186d58fb5f860fcf89cea142c9 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 30 Aug 2023 09:54:37 +0200 Subject: [PATCH 197/327] Deadline: Tweak logging for artist-facing reports in publisher --- .../publish/collect_deadline_server_from_instance.py | 2 +- .../deadline/plugins/publish/submit_celaction_deadline.py | 6 +++--- .../deadline/plugins/publish/submit_fusion_deadline.py | 4 ++-- .../deadline/plugins/publish/submit_harmony_deadline.py | 2 +- .../plugins/publish/submit_houdini_remote_publish.py | 2 +- .../deadline/plugins/publish/submit_maya_deadline.py | 6 +++--- .../deadline/plugins/publish/submit_nuke_deadline.py | 8 ++++---- .../deadline/plugins/publish/submit_publish_job.py | 4 ++-- .../plugins/publish/validate_deadline_connection.py | 2 +- .../deadline/plugins/publish/validate_deadline_pools.py | 4 ++-- 10 files changed, 20 insertions(+), 20 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/collect_deadline_server_from_instance.py b/openpype/modules/deadline/plugins/publish/collect_deadline_server_from_instance.py index eadfc3c83e..98ecb019c5 100644 --- a/openpype/modules/deadline/plugins/publish/collect_deadline_server_from_instance.py +++ b/openpype/modules/deadline/plugins/publish/collect_deadline_server_from_instance.py @@ -23,7 +23,7 @@ class CollectDeadlineServerFromInstance(pyblish.api.InstancePlugin): instance.data["deadlineUrl"] = self._collect_deadline_url(instance) instance.data["deadlineUrl"] = \ instance.data["deadlineUrl"].strip().rstrip("/") - self.log.info( + self.log.debug( "Using {} for submission.".format(instance.data["deadlineUrl"])) def _collect_deadline_url(self, render_instance): diff --git a/openpype/modules/deadline/plugins/publish/submit_celaction_deadline.py b/openpype/modules/deadline/plugins/publish/submit_celaction_deadline.py index ee28612b44..5a6537a767 100644 --- a/openpype/modules/deadline/plugins/publish/submit_celaction_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_celaction_deadline.py @@ -183,10 +183,10 @@ class CelactionSubmitDeadline(pyblish.api.InstancePlugin): } plugin = payload["JobInfo"]["Plugin"] - self.log.info("using render plugin : {}".format(plugin)) + self.log.debug("using render plugin : {}".format(plugin)) - self.log.info("Submitting..") - self.log.info(json.dumps(payload, indent=4, sort_keys=True)) + self.log.debug("Submitting..") + self.log.debug(json.dumps(payload, indent=4, sort_keys=True)) # adding expectied files to instance.data self.expected_files(instance, render_path) diff --git a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py index a48596c6bf..70aa12956d 100644 --- a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py @@ -233,8 +233,8 @@ class FusionSubmitDeadline( ) for index, key in enumerate(environment) }) - self.log.info("Submitting..") - self.log.info(json.dumps(payload, indent=4, sort_keys=True)) + self.log.debug("Submitting..") + self.log.debug(json.dumps(payload, indent=4, sort_keys=True)) # E.g. http://192.168.0.1:8082/api/jobs url = "{}/api/jobs".format(deadline_url) diff --git a/openpype/modules/deadline/plugins/publish/submit_harmony_deadline.py b/openpype/modules/deadline/plugins/publish/submit_harmony_deadline.py index 2c37268f04..9f8579cd23 100644 --- a/openpype/modules/deadline/plugins/publish/submit_harmony_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_harmony_deadline.py @@ -369,7 +369,7 @@ class HarmonySubmitDeadline( # rendering, we need to unzip it. published_scene = Path( self.from_published_scene(False)) - self.log.info(f"Processing {published_scene.as_posix()}") + self.log.debug(f"Processing {published_scene.as_posix()}") xstage_path = self._unzip_scene_file(published_scene) render_path = xstage_path.parent / "renders" diff --git a/openpype/modules/deadline/plugins/publish/submit_houdini_remote_publish.py b/openpype/modules/deadline/plugins/publish/submit_houdini_remote_publish.py index 68aa653804..39c0c3afe4 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_remote_publish.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_remote_publish.py @@ -162,7 +162,7 @@ class HoudiniSubmitPublishDeadline(pyblish.api.ContextPlugin): ) # Submit - self.log.info("Submitting..") + self.log.debug("Submitting..") self.log.debug(json.dumps(payload, indent=4, sort_keys=True)) # E.g. http://192.168.0.1:8082/api/jobs diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py index 75d24b28f0..615d167a9f 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -434,7 +434,7 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, new_job_info.update(tiles_data["JobInfo"]) new_plugin_info.update(tiles_data["PluginInfo"]) - self.log.info("hashing {} - {}".format(file_index, file)) + self.log.debug("hashing {} - {}".format(file_index, file)) job_hash = hashlib.sha256( ("{}_{}".format(file_index, file)).encode("utf-8")) @@ -450,7 +450,7 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, ) file_index += 1 - self.log.info( + self.log.debug( "Submitting tile job(s) [{}] ...".format(len(frame_payloads))) # Submit frame tile jobs @@ -560,7 +560,7 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, assembly_job_ids = [] num_assemblies = len(assembly_payloads) for i, payload in enumerate(assembly_payloads): - self.log.info( + self.log.debug( "submitting assembly job {} of {}".format(i + 1, num_assemblies) ) diff --git a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py index cfdeb4968b..ffa203e922 100644 --- a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py @@ -244,7 +244,7 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin, # resolve any limit groups limit_groups = self.get_limit_groups() - self.log.info("Limit groups: `{}`".format(limit_groups)) + self.log.debug("Limit groups: `{}`".format(limit_groups)) payload = { "JobInfo": { @@ -387,10 +387,10 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin, }) plugin = payload["JobInfo"]["Plugin"] - self.log.info("using render plugin : {}".format(plugin)) + self.log.debug("using render plugin : {}".format(plugin)) - self.log.info("Submitting..") - self.log.info(json.dumps(payload, indent=4, sort_keys=True)) + self.log.debug("Submitting..") + self.log.debug(json.dumps(payload, indent=4, sort_keys=True)) # adding expectied files to instance.data self.expected_files( diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index bf4411ef43..20bebe583f 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -317,7 +317,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, # remove secondary pool payload["JobInfo"].pop("SecondaryPool", None) - self.log.info("Submitting Deadline job ...") + self.log.debug("Submitting Deadline publish job ...") url = "{}/api/jobs".format(self.deadline_url) response = requests.post(url, json=payload, timeout=10) @@ -454,7 +454,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, import getpass render_job = {} - self.log.info("Faking job data ...") + self.log.debug("Faking job data ...") render_job["Props"] = {} # Render job doesn't exist because we do not have prior submission. # We still use data from it so lets fake it. diff --git a/openpype/modules/deadline/plugins/publish/validate_deadline_connection.py b/openpype/modules/deadline/plugins/publish/validate_deadline_connection.py index a30401e7dc..d8933a191f 100644 --- a/openpype/modules/deadline/plugins/publish/validate_deadline_connection.py +++ b/openpype/modules/deadline/plugins/publish/validate_deadline_connection.py @@ -18,7 +18,7 @@ class ValidateDeadlineConnection(pyblish.api.InstancePlugin): # if custom one is set in instance, use that if instance.data.get("deadlineUrl"): deadline_url = instance.data.get("deadlineUrl") - self.log.info( + self.log.debug( "We have deadline URL on instance {}".format( deadline_url)) assert deadline_url, "Requires Deadline Webservice URL" diff --git a/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py b/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py index 594f0ef866..1179b9cee9 100644 --- a/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py +++ b/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py @@ -32,9 +32,9 @@ class ValidateDeadlinePools(OptionalPyblishPluginMixin, # get default deadline webservice url from deadline module deadline_url = instance.context.data["defaultDeadline"] - self.log.info("deadline_url::{}".format(deadline_url)) + self.log.debug("deadline_url::{}".format(deadline_url)) pools = DeadlineModule.get_deadline_pools(deadline_url, log=self.log) - self.log.info("pools::{}".format(pools)) + self.log.debug("pools::{}".format(pools)) formatting_data = { "pools_str": ",".join(pools) From 63923ff4d8c65fde9b984ac8f0e1d8973d127ef6 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 30 Aug 2023 10:10:33 +0200 Subject: [PATCH 198/327] Global: Tweak logging for artist-facing reports in publisher --- openpype/plugins/publish/cleanup.py | 6 +++-- openpype/plugins/publish/cleanup_farm.py | 8 +++--- openpype/plugins/publish/collect_audio.py | 8 +++--- openpype/plugins/publish/collect_hierarchy.py | 2 +- .../plugins/publish/collect_rendered_files.py | 12 ++++----- openpype/plugins/publish/extract_burnin.py | 18 ++++++------- .../publish/extract_color_transcode.py | 2 +- .../publish/extract_colorspace_data.py | 2 +- .../publish/extract_hierarchy_avalon.py | 2 +- .../publish/extract_hierarchy_to_ayon.py | 4 +-- .../plugins/publish/extract_review_slate.py | 4 +-- .../plugins/publish/extract_scanline_exr.py | 12 ++++----- openpype/plugins/publish/extract_thumbnail.py | 26 +++++++++---------- .../publish/extract_thumbnail_from_source.py | 14 +++++----- .../publish/extract_trim_video_audio.py | 6 ++--- openpype/plugins/publish/integrate.py | 6 ++--- .../plugins/publish/integrate_hero_version.py | 6 ++--- .../plugins/publish/integrate_thumbnail.py | 8 +++--- .../publish/integrate_thumbnail_ayon.py | 6 ++--- .../plugins/publish/validate_asset_docs.py | 4 +-- .../publish/validate_editorial_asset_name.py | 2 +- 21 files changed, 81 insertions(+), 77 deletions(-) diff --git a/openpype/plugins/publish/cleanup.py b/openpype/plugins/publish/cleanup.py index 573cd829e4..f9f504dd4f 100644 --- a/openpype/plugins/publish/cleanup.py +++ b/openpype/plugins/publish/cleanup.py @@ -69,7 +69,7 @@ class CleanUp(pyblish.api.InstancePlugin): skip_cleanup_filepaths.add(os.path.normpath(path)) if self.remove_temp_renders: - self.log.info("Cleaning renders new...") + self.log.debug("Cleaning renders new...") self.clean_renders(instance, skip_cleanup_filepaths) if [ef for ef in self.exclude_families @@ -95,7 +95,9 @@ class CleanUp(pyblish.api.InstancePlugin): return if instance.data.get("stagingDir_persistent"): - self.log.info("Staging dir: %s should be persistent" % staging_dir) + self.log.debug( + "Staging dir {} should be persistent".format(staging_dir) + ) return self.log.info("Removing staging directory {}".format(staging_dir)) diff --git a/openpype/plugins/publish/cleanup_farm.py b/openpype/plugins/publish/cleanup_farm.py index 8052f13734..e655437ced 100644 --- a/openpype/plugins/publish/cleanup_farm.py +++ b/openpype/plugins/publish/cleanup_farm.py @@ -26,10 +26,10 @@ class CleanUpFarm(pyblish.api.ContextPlugin): # Skip process if is not in list of source hosts in which this # plugin should run if src_host_name not in self.allowed_hosts: - self.log.info(( + self.log.debug( "Source host \"{}\" is not in list of enabled hosts {}." - " Skipping" - ).format(str(src_host_name), str(self.allowed_hosts))) + " Skipping".format(src_host_name, self.allowed_hosts) + ) return self.log.debug("Preparing filepaths to remove") @@ -47,7 +47,7 @@ class CleanUpFarm(pyblish.api.ContextPlugin): dirpaths_to_remove.add(os.path.normpath(staging_dir)) if not dirpaths_to_remove: - self.log.info("Nothing to remove. Skipping") + self.log.debug("Nothing to remove. Skipping") return self.log.debug("Filepaths to remove are:\n{}".format( diff --git a/openpype/plugins/publish/collect_audio.py b/openpype/plugins/publish/collect_audio.py index 3a0ddb3281..6aaadfc568 100644 --- a/openpype/plugins/publish/collect_audio.py +++ b/openpype/plugins/publish/collect_audio.py @@ -53,8 +53,8 @@ class CollectAudio(pyblish.api.ContextPlugin): ): # Skip instances that already have audio filled if instance.data.get("audio"): - self.log.info( - "Skipping Audio collecion. It is already collected" + self.log.debug( + "Skipping Audio collection. It is already collected" ) continue filtered_instances.append(instance) @@ -70,7 +70,7 @@ class CollectAudio(pyblish.api.ContextPlugin): instances_by_asset_name[asset_name].append(instance) asset_names = set(instances_by_asset_name.keys()) - self.log.info(( + self.log.debug(( "Searching for audio subset '{subset}' in assets {assets}" ).format( subset=self.audio_subset_name, @@ -100,7 +100,7 @@ class CollectAudio(pyblish.api.ContextPlugin): "offset": 0, "filename": repre_path }] - self.log.info("Audio Data added to instance ...") + self.log.debug("Audio Data added to instance ...") def query_representations(self, project_name, asset_names): """Query representations related to audio subsets for passed assets. diff --git a/openpype/plugins/publish/collect_hierarchy.py b/openpype/plugins/publish/collect_hierarchy.py index 687397be8a..b5fd1e4bb9 100644 --- a/openpype/plugins/publish/collect_hierarchy.py +++ b/openpype/plugins/publish/collect_hierarchy.py @@ -24,7 +24,7 @@ class CollectHierarchy(pyblish.api.ContextPlugin): final_context[project_name]['entity_type'] = 'Project' for instance in context: - self.log.info("Processing instance: `{}` ...".format(instance)) + self.log.debug("Processing instance: `{}` ...".format(instance)) # shot data dict shot_data = {} diff --git a/openpype/plugins/publish/collect_rendered_files.py b/openpype/plugins/publish/collect_rendered_files.py index 6c8d1e9ca5..aaf290ace7 100644 --- a/openpype/plugins/publish/collect_rendered_files.py +++ b/openpype/plugins/publish/collect_rendered_files.py @@ -91,12 +91,12 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin): # now we can just add instances from json file and we are done for instance_data in data.get("instances"): - self.log.info(" - processing instance for {}".format( + self.log.debug(" - processing instance for {}".format( instance_data.get("subset"))) instance = self._context.create_instance( instance_data.get("subset") ) - self.log.info("Filling stagingDir...") + self.log.debug("Filling stagingDir...") self._fill_staging_dir(instance_data, anatomy) instance.data.update(instance_data) @@ -121,7 +121,7 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin): "offset": 0 }] }) - self.log.info( + self.log.debug( f"Adding audio to instance: {instance.data['audio']}") def process(self, context): @@ -137,11 +137,11 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin): # Using already collected Anatomy anatomy = context.data["anatomy"] - self.log.info("Getting root setting for project \"{}\"".format( + self.log.debug("Getting root setting for project \"{}\"".format( anatomy.project_name )) - self.log.info("anatomy: {}".format(anatomy.roots)) + self.log.debug("anatomy: {}".format(anatomy.roots)) try: session_is_set = False for path in paths: @@ -156,7 +156,7 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin): if remapped: session_data["AVALON_WORKDIR"] = remapped - self.log.info("Setting session using data from file") + self.log.debug("Setting session using data from file") legacy_io.Session.update(session_data) os.environ.update(session_data) session_is_set = True diff --git a/openpype/plugins/publish/extract_burnin.py b/openpype/plugins/publish/extract_burnin.py index e5b37ee3b4..dc8aab6ce4 100644 --- a/openpype/plugins/publish/extract_burnin.py +++ b/openpype/plugins/publish/extract_burnin.py @@ -83,7 +83,7 @@ class ExtractBurnin(publish.Extractor): return if not instance.data.get("representations"): - self.log.info( + self.log.debug( "Instance does not have filled representations. Skipping") return @@ -135,11 +135,11 @@ class ExtractBurnin(publish.Extractor): burnin_defs, repre["tags"] ) if not repre_burnin_defs: - self.log.info(( + self.log.debug( "Skipped representation. All burnin definitions from" - " selected profile does not match to representation's" - " tags. \"{}\"" - ).format(str(repre["tags"]))) + " selected profile do not match to representation's" + " tags. \"{}\"".format(repre["tags"]) + ) continue filtered_repres.append((repre, repre_burnin_defs)) @@ -164,7 +164,7 @@ class ExtractBurnin(publish.Extractor): logger=self.log) if not profile: - self.log.info(( + self.log.debug(( "Skipped instance. None of profiles in presets are for" " Host: \"{}\" | Families: \"{}\" | Task \"{}\"" " | Task type \"{}\" | Subset \"{}\" " @@ -176,7 +176,7 @@ class ExtractBurnin(publish.Extractor): # Pre-filter burnin definitions by instance families burnin_defs = self.filter_burnins_defs(profile, instance) if not burnin_defs: - self.log.info(( + self.log.debug(( "Skipped instance. Burnin definitions are not set for profile" " Host: \"{}\" | Families: \"{}\" | Task \"{}\"" " | Profile \"{}\"" @@ -223,10 +223,10 @@ class ExtractBurnin(publish.Extractor): # If result is None the requirement of conversion can't be # determined if do_convert is None: - self.log.info(( + self.log.debug( "Can't determine if representation requires conversion." " Skipped." - )) + ) continue # Do conversion if needed diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index f7c8af9318..dbf1b6c8a6 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -320,7 +320,7 @@ class ExtractOIIOTranscode(publish.Extractor): logger=self.log) if not profile: - self.log.info(( + self.log.debug(( "Skipped instance. None of profiles in presets are for" " Host: \"{}\" | Families: \"{}\" | Task \"{}\"" " | Task type \"{}\" | Subset \"{}\" " diff --git a/openpype/plugins/publish/extract_colorspace_data.py b/openpype/plugins/publish/extract_colorspace_data.py index 363df28fb5..8873dcd637 100644 --- a/openpype/plugins/publish/extract_colorspace_data.py +++ b/openpype/plugins/publish/extract_colorspace_data.py @@ -30,7 +30,7 @@ class ExtractColorspaceData(publish.Extractor, def process(self, instance): representations = instance.data.get("representations") if not representations: - self.log.info("No representations at instance : `{}`".format( + self.log.debug("No representations at instance : `{}`".format( instance)) return diff --git a/openpype/plugins/publish/extract_hierarchy_avalon.py b/openpype/plugins/publish/extract_hierarchy_avalon.py index 1d57545bc0..d70f0cbdd7 100644 --- a/openpype/plugins/publish/extract_hierarchy_avalon.py +++ b/openpype/plugins/publish/extract_hierarchy_avalon.py @@ -21,7 +21,7 @@ class ExtractHierarchyToAvalon(pyblish.api.ContextPlugin): return if "hierarchyContext" not in context.data: - self.log.info("skipping IntegrateHierarchyToAvalon") + self.log.debug("skipping ExtractHierarchyToAvalon") return if not legacy_io.Session: diff --git a/openpype/plugins/publish/extract_hierarchy_to_ayon.py b/openpype/plugins/publish/extract_hierarchy_to_ayon.py index 915650ae41..bb0081b7a5 100644 --- a/openpype/plugins/publish/extract_hierarchy_to_ayon.py +++ b/openpype/plugins/publish/extract_hierarchy_to_ayon.py @@ -27,13 +27,13 @@ class ExtractHierarchyToAYON(pyblish.api.ContextPlugin): hierarchy_context = context.data.get("hierarchyContext") if not hierarchy_context: - self.log.info("Skipping") + self.log.debug("Skipping ExtractHierarchyToAYON") return project_name = context.data["projectName"] hierarchy_context = self._filter_hierarchy(context) if not hierarchy_context: - self.log.info("All folders were filtered out") + self.log.debug("All folders were filtered out") return self.log.debug("Hierarchy_context: {}".format( diff --git a/openpype/plugins/publish/extract_review_slate.py b/openpype/plugins/publish/extract_review_slate.py index 886384fee6..8b1a06b6b8 100644 --- a/openpype/plugins/publish/extract_review_slate.py +++ b/openpype/plugins/publish/extract_review_slate.py @@ -46,7 +46,7 @@ class ExtractReviewSlate(publish.Extractor): "*": inst_data["slateFrame"] } - self.log.info("_ slates_data: {}".format(pformat(slates_data))) + self.log.debug("_ slates_data: {}".format(pformat(slates_data))) if "reviewToWidth" in inst_data: use_legacy_code = True @@ -76,7 +76,7 @@ class ExtractReviewSlate(publish.Extractor): ) # get slate data slate_path = self._get_slate_path(input_file, slates_data) - self.log.info("_ slate_path: {}".format(slate_path)) + self.log.debug("_ slate_path: {}".format(slate_path)) slate_width, slate_height = self._get_slates_resolution(slate_path) diff --git a/openpype/plugins/publish/extract_scanline_exr.py b/openpype/plugins/publish/extract_scanline_exr.py index 9f22794a79..747155689b 100644 --- a/openpype/plugins/publish/extract_scanline_exr.py +++ b/openpype/plugins/publish/extract_scanline_exr.py @@ -29,24 +29,24 @@ class ExtractScanlineExr(pyblish.api.InstancePlugin): representations_new = [] for repre in representations: - self.log.info( + self.log.debug( "Processing representation {}".format(repre.get("name"))) tags = repre.get("tags", []) if "toScanline" not in tags: - self.log.info(" - missing toScanline tag") + self.log.debug(" - missing toScanline tag") continue # run only on exrs if repre.get("ext") != "exr": - self.log.info("- not EXR files") + self.log.debug("- not EXR files") continue if not isinstance(repre['files'], (list, tuple)): input_files = [repre['files']] - self.log.info("We have a single frame") + self.log.debug("We have a single frame") else: input_files = repre['files'] - self.log.info("We have a sequence") + self.log.debug("We have a sequence") stagingdir = os.path.normpath(repre.get("stagingDir")) @@ -68,7 +68,7 @@ class ExtractScanlineExr(pyblish.api.InstancePlugin): ] subprocess_exr = " ".join(oiio_cmd) - self.log.info(f"running: {subprocess_exr}") + self.log.debug(f"running: {subprocess_exr}") run_subprocess(subprocess_exr, logger=self.log) # raise error if there is no ouptput diff --git a/openpype/plugins/publish/extract_thumbnail.py b/openpype/plugins/publish/extract_thumbnail.py index b72a6d02ad..cc2892213a 100644 --- a/openpype/plugins/publish/extract_thumbnail.py +++ b/openpype/plugins/publish/extract_thumbnail.py @@ -43,12 +43,12 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): # Skip if instance have 'review' key in data set to 'False' if not self._is_review_instance(instance): - self.log.info("Skipping - no review set on instance.") + self.log.debug("Skipping - no review set on instance.") return # Check if already has thumbnail created if self._already_has_thumbnail(instance_repres): - self.log.info("Thumbnail representation already present.") + self.log.debug("Thumbnail representation already present.") return # skip crypto passes. @@ -58,15 +58,15 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): # representation that can be determined much earlier and # with better precision. if "crypto" in subset_name.lower(): - self.log.info("Skipping crypto passes.") + self.log.debug("Skipping crypto passes.") return filtered_repres = self._get_filtered_repres(instance) if not filtered_repres: - self.log.info(( - "Instance don't have representations" - " that can be used as source for thumbnail. Skipping" - )) + self.log.info( + "Instance doesn't have representations that can be used " + "as source for thumbnail. Skipping thumbnail extraction." + ) return # Create temp directory for thumbnail @@ -107,10 +107,10 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): # oiiotool isn't available if not thumbnail_created: if oiio_supported: - self.log.info(( + self.log.debug( "Converting with FFMPEG because input" " can't be read by OIIO." - )) + ) thumbnail_created = self.create_thumbnail_ffmpeg( full_input_path, full_output_path @@ -165,8 +165,8 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): continue if not repre.get("files"): - self.log.info(( - "Representation \"{}\" don't have files. Skipping" + self.log.debug(( + "Representation \"{}\" doesn't have files. Skipping" ).format(repre["name"])) continue @@ -174,7 +174,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): return filtered_repres def create_thumbnail_oiio(self, src_path, dst_path): - self.log.info("Extracting thumbnail {}".format(dst_path)) + self.log.debug("Extracting thumbnail with OIIO: {}".format(dst_path)) oiio_cmd = get_oiio_tool_args( "oiiotool", "-a", src_path, @@ -192,7 +192,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): return False def create_thumbnail_ffmpeg(self, src_path, dst_path): - self.log.info("outputting {}".format(dst_path)) + self.log.debug("Extracting thumbnail with FFMPEG: {}".format(dst_path)) ffmpeg_path_args = get_ffmpeg_tool_args("ffmpeg") ffmpeg_args = self.ffmpeg_args or {} diff --git a/openpype/plugins/publish/extract_thumbnail_from_source.py b/openpype/plugins/publish/extract_thumbnail_from_source.py index 1b9f0a8bae..c1f271a8cc 100644 --- a/openpype/plugins/publish/extract_thumbnail_from_source.py +++ b/openpype/plugins/publish/extract_thumbnail_from_source.py @@ -49,7 +49,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): # Check if already has thumbnail created if self._instance_has_thumbnail(instance): - self.log.info("Thumbnail representation already present.") + self.log.debug("Thumbnail representation already present.") return dst_filepath = self._create_thumbnail( @@ -98,7 +98,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): thumbnail_created = False oiio_supported = is_oiio_supported() - self.log.info("Thumbnail source: {}".format(thumbnail_source)) + self.log.debug("Thumbnail source: {}".format(thumbnail_source)) src_basename = os.path.basename(thumbnail_source) dst_filename = os.path.splitext(src_basename)[0] + "_thumb.jpg" full_output_path = os.path.join(dst_staging, dst_filename) @@ -115,10 +115,10 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): # oiiotool isn't available if not thumbnail_created: if oiio_supported: - self.log.info(( + self.log.info( "Converting with FFMPEG because input" " can't be read by OIIO." - )) + ) thumbnail_created = self.create_thumbnail_ffmpeg( thumbnail_source, full_output_path @@ -143,14 +143,14 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): return False def create_thumbnail_oiio(self, src_path, dst_path): - self.log.info("outputting {}".format(dst_path)) + self.log.debug("Outputting thumbnail with OIIO: {}".format(dst_path)) oiio_cmd = get_oiio_tool_args( "oiiotool", "-a", src_path, "--ch", "R,G,B", "-o", dst_path ) - self.log.info("Running: {}".format(" ".join(oiio_cmd))) + self.log.debug("Running: {}".format(" ".join(oiio_cmd))) try: run_subprocess(oiio_cmd, logger=self.log) return True @@ -173,7 +173,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): dst_path ) - self.log.info("Running: {}".format(" ".join(ffmpeg_cmd))) + self.log.debug("Running: {}".format(" ".join(ffmpeg_cmd))) try: run_subprocess(ffmpeg_cmd, logger=self.log) return True diff --git a/openpype/plugins/publish/extract_trim_video_audio.py b/openpype/plugins/publish/extract_trim_video_audio.py index 2907ae1839..5e00cfc96f 100644 --- a/openpype/plugins/publish/extract_trim_video_audio.py +++ b/openpype/plugins/publish/extract_trim_video_audio.py @@ -36,7 +36,7 @@ class ExtractTrimVideoAudio(publish.Extractor): # get staging dir staging_dir = self.staging_dir(instance) - self.log.info("Staging dir set to: `{}`".format(staging_dir)) + self.log.debug("Staging dir set to: `{}`".format(staging_dir)) # Generate mov file. fps = instance.data["fps"] @@ -59,7 +59,7 @@ class ExtractTrimVideoAudio(publish.Extractor): extensions = [output_file_type] for ext in extensions: - self.log.info("Processing ext: `{}`".format(ext)) + self.log.debug("Processing ext: `{}`".format(ext)) if not ext.startswith("."): ext = "." + ext @@ -98,7 +98,7 @@ class ExtractTrimVideoAudio(publish.Extractor): ffmpeg_args.append(clip_trimed_path) joined_args = " ".join(ffmpeg_args) - self.log.info(f"Processing: {joined_args}") + self.log.debug(f"Processing: {joined_args}") run_subprocess( ffmpeg_args, logger=self.log ) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index be07cffe72..0c18ab4466 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -155,13 +155,13 @@ class IntegrateAsset(pyblish.api.InstancePlugin): # Instance should be integrated on a farm if instance.data.get("farm"): - self.log.info( + self.log.debug( "Instance is marked to be processed on farm. Skipping") return # Instance is marked to not get integrated if not instance.data.get("integrate", True): - self.log.info("Instance is marked to skip integrating. Skipping") + self.log.debug("Instance is marked to skip integrating. Skipping") return filtered_repres = self.filter_representations(instance) @@ -306,7 +306,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): # increase if the file transaction takes a long time. op_session.commit() - self.log.info("Subset {subset[name]} and Version {version[name]} " + self.log.info("Subset '{subset[name]}' and Version {version[name]} " "written to database..".format(subset=subset, version=version)) diff --git a/openpype/plugins/publish/integrate_hero_version.py b/openpype/plugins/publish/integrate_hero_version.py index 6c21664b78..9f0f7fe7f3 100644 --- a/openpype/plugins/publish/integrate_hero_version.py +++ b/openpype/plugins/publish/integrate_hero_version.py @@ -275,10 +275,10 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): backup_hero_publish_dir = _backup_hero_publish_dir break except Exception: - self.log.info(( + self.log.info( "Could not remove previous backup folder." - " Trying to add index to folder name" - )) + " Trying to add index to folder name." + ) _backup_hero_publish_dir = ( backup_hero_publish_dir + str(idx) diff --git a/openpype/plugins/publish/integrate_thumbnail.py b/openpype/plugins/publish/integrate_thumbnail.py index 9929d8f754..0c12255d38 100644 --- a/openpype/plugins/publish/integrate_thumbnail.py +++ b/openpype/plugins/publish/integrate_thumbnail.py @@ -41,7 +41,9 @@ class IntegrateThumbnails(pyblish.api.ContextPlugin): def process(self, context): if AYON_SERVER_ENABLED: - self.log.info("AYON is enabled. Skipping v3 thumbnail integration") + self.log.debug( + "AYON is enabled. Skipping v3 thumbnail integration" + ) return # Filter instances which can be used for integration @@ -74,14 +76,14 @@ class IntegrateThumbnails(pyblish.api.ContextPlugin): thumbnail_template = anatomy.templates["publish"]["thumbnail"] if not thumbnail_template: - self.log.info("Thumbnail template is not filled. Skipping.") + self.log.debug("Thumbnail template is not filled. Skipping.") return if ( not thumbnail_root and thumbnail_root_format_key in thumbnail_template ): - self.log.warning(("{} is not set. Skipping.").format(env_key)) + self.log.warning("{} is not set. Skipping.".format(env_key)) return # Collect verion ids from all filtered instance diff --git a/openpype/plugins/publish/integrate_thumbnail_ayon.py b/openpype/plugins/publish/integrate_thumbnail_ayon.py index ba5664c69f..cf05327ce8 100644 --- a/openpype/plugins/publish/integrate_thumbnail_ayon.py +++ b/openpype/plugins/publish/integrate_thumbnail_ayon.py @@ -35,13 +35,13 @@ class IntegrateThumbnailsAYON(pyblish.api.ContextPlugin): def process(self, context): if not AYON_SERVER_ENABLED: - self.log.info("AYON is not enabled. Skipping") + self.log.debug("AYON is not enabled. Skipping") return # Filter instances which can be used for integration filtered_instance_items = self._prepare_instances(context) if not filtered_instance_items: - self.log.info( + self.log.debug( "All instances were filtered. Thumbnail integration skipped." ) return @@ -110,7 +110,7 @@ class IntegrateThumbnailsAYON(pyblish.api.ContextPlugin): # Skip instance if thumbnail path is not available for it if not thumbnail_path: - self.log.info(( + self.log.debug(( "Skipping thumbnail integration for instance \"{}\"." " Instance and context" " thumbnail paths are not available." diff --git a/openpype/plugins/publish/validate_asset_docs.py b/openpype/plugins/publish/validate_asset_docs.py index 9a1ca5b8de..8dfd783c39 100644 --- a/openpype/plugins/publish/validate_asset_docs.py +++ b/openpype/plugins/publish/validate_asset_docs.py @@ -22,11 +22,11 @@ class ValidateAssetDocs(pyblish.api.InstancePlugin): return if instance.data.get("assetEntity"): - self.log.info("Instance has set asset document in its data.") + self.log.debug("Instance has set asset document in its data.") elif instance.data.get("newAssetPublishing"): # skip if it is editorial - self.log.info("Editorial instance is no need to check...") + self.log.debug("Editorial instance has no need to check...") else: raise PublishValidationError(( diff --git a/openpype/plugins/publish/validate_editorial_asset_name.py b/openpype/plugins/publish/validate_editorial_asset_name.py index 4f8a1abf2e..fca0d8e7f5 100644 --- a/openpype/plugins/publish/validate_editorial_asset_name.py +++ b/openpype/plugins/publish/validate_editorial_asset_name.py @@ -56,7 +56,7 @@ class ValidateEditorialAssetName(pyblish.api.ContextPlugin): } continue - self.log.info("correct asset: {}".format(asset)) + self.log.debug("correct asset: {}".format(asset)) if assets_missing_name: wrong_names = {} From 7243f7e7219cd59aebb5c970666bb5d8552d378b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 30 Aug 2023 10:11:30 +0200 Subject: [PATCH 199/327] Fix context not being reported correctly if it was already set on `context.data` prior to collector --- openpype/plugins/publish/collect_current_context.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/collect_current_context.py b/openpype/plugins/publish/collect_current_context.py index 166d75e5de..8b12a3f77f 100644 --- a/openpype/plugins/publish/collect_current_context.py +++ b/openpype/plugins/publish/collect_current_context.py @@ -39,5 +39,12 @@ class CollectCurrentContext(pyblish.api.ContextPlugin): # - 'task' -> 'taskName' self.log.info(( - "Collected project context\nProject: {}\nAsset: {}\nTask: {}" - ).format(project_name, asset_name, task_name)) + "Collected project context\n" + "Project: {project_name}\n" + "Asset: {asset_name}\n" + "Task: {task_name}" + ).format( + project_name=context.data["projectName"], + asset_name=context.data["asset"], + task_name=context.data["task"] + )) From d03d0564cdd471ce8ac3c0fdcf85db4841c69508 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 30 Aug 2023 10:13:07 +0200 Subject: [PATCH 200/327] Fix typo `thubmnail` -> `thumbnail` --- openpype/plugins/publish/extract_thumbnail.py | 2 +- openpype/plugins/publish/extract_thumbnail_from_source.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/plugins/publish/extract_thumbnail.py b/openpype/plugins/publish/extract_thumbnail.py index cc2892213a..de101ac7ac 100644 --- a/openpype/plugins/publish/extract_thumbnail.py +++ b/openpype/plugins/publish/extract_thumbnail.py @@ -225,7 +225,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): return True except Exception: self.log.warning( - "Failed to create thubmnail using ffmpeg", + "Failed to create thumbnail using ffmpeg", exc_info=True ) return False diff --git a/openpype/plugins/publish/extract_thumbnail_from_source.py b/openpype/plugins/publish/extract_thumbnail_from_source.py index c1f271a8cc..401a5d615d 100644 --- a/openpype/plugins/publish/extract_thumbnail_from_source.py +++ b/openpype/plugins/publish/extract_thumbnail_from_source.py @@ -156,7 +156,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): return True except Exception: self.log.warning( - "Failed to create thubmnail using oiiotool", + "Failed to create thumbnail using oiiotool", exc_info=True ) return False @@ -179,7 +179,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): return True except Exception: self.log.warning( - "Failed to create thubmnail using ffmpeg", + "Failed to create thumbnail using ffmpeg", exc_info=True ) return False From 2ba42fe376bb6b0fec30cc1a37fb634b7ba69318 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 30 Aug 2023 10:14:53 +0200 Subject: [PATCH 201/327] Tweak logging level --- openpype/hosts/fusion/plugins/publish/collect_instances.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_instances.py b/openpype/hosts/fusion/plugins/publish/collect_instances.py index 6016baa2a9..4d6da79b77 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_instances.py +++ b/openpype/hosts/fusion/plugins/publish/collect_instances.py @@ -85,5 +85,5 @@ class CollectInstanceData(pyblish.api.InstancePlugin): # Add review family if the instance is marked as 'review' # This could be done through a 'review' Creator attribute. if instance.data.get("review", False): - self.log.info("Adding review family..") + self.log.debug("Adding review family..") instance.data["families"].append("review") From ca8305a0cf2a48bb5c9965c6cab9b4bc14aaf549 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 30 Aug 2023 10:16:37 +0200 Subject: [PATCH 202/327] Tweak logging level of removal of directory in tempdir to debug since artist doesn't care about seeing that in artist facing report --- openpype/plugins/publish/cleanup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/cleanup.py b/openpype/plugins/publish/cleanup.py index f9f504dd4f..6c122ddf09 100644 --- a/openpype/plugins/publish/cleanup.py +++ b/openpype/plugins/publish/cleanup.py @@ -100,7 +100,7 @@ class CleanUp(pyblish.api.InstancePlugin): ) return - self.log.info("Removing staging directory {}".format(staging_dir)) + self.log.debug("Removing staging directory {}".format(staging_dir)) shutil.rmtree(staging_dir) def clean_renders(self, instance, skip_cleanup_filepaths): From e9cb011c2a38084bc2318e4b15d011e2278f0c38 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 30 Aug 2023 10:31:06 +0200 Subject: [PATCH 203/327] Remove unused attribute --- openpype/hosts/maya/plugins/publish/collect_history.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_history.py b/openpype/hosts/maya/plugins/publish/collect_history.py index 71f0169971..d4e8c6298b 100644 --- a/openpype/hosts/maya/plugins/publish/collect_history.py +++ b/openpype/hosts/maya/plugins/publish/collect_history.py @@ -18,7 +18,6 @@ class CollectMayaHistory(pyblish.api.InstancePlugin): hosts = ["maya"] label = "Maya History" families = ["rig"] - verbose = False def process(self, instance): From 97f9cd4377e83cfec48ae8d18bcc90d783185e60 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 30 Aug 2023 14:14:33 +0200 Subject: [PATCH 204/327] Do not warn about renderlayer instance being empty --- openpype/hosts/maya/plugins/publish/collect_instances.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_instances.py b/openpype/hosts/maya/plugins/publish/collect_instances.py index 5f914b40d7..5058da3d01 100644 --- a/openpype/hosts/maya/plugins/publish/collect_instances.py +++ b/openpype/hosts/maya/plugins/publish/collect_instances.py @@ -28,6 +28,8 @@ class CollectNewInstances(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder hosts = ["maya"] + valid_empty_families = {"workfile", "renderlayer"} + def process(self, instance): objset = instance.data.get("instance_node") @@ -58,7 +60,7 @@ class CollectNewInstances(pyblish.api.InstancePlugin): instance[:] = members_hierarchy - elif instance.data["family"] != "workfile": + elif instance.data["family"] not in self.valid_empty_families: self.log.warning("Empty instance: \"%s\" " % objset) # Store the exact members of the object set instance.data["setMembers"] = members From 0fa081dddb6e6295748f338daa822e1e2cd61d40 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 30 Aug 2023 14:16:27 +0200 Subject: [PATCH 205/327] Ignore arnold attribute check if arnold is not loaded --- .../maya/plugins/publish/validate_mesh_arnold_attributes.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/validate_mesh_arnold_attributes.py b/openpype/hosts/maya/plugins/publish/validate_mesh_arnold_attributes.py index 55624726ea..59ba326d45 100644 --- a/openpype/hosts/maya/plugins/publish/validate_mesh_arnold_attributes.py +++ b/openpype/hosts/maya/plugins/publish/validate_mesh_arnold_attributes.py @@ -109,6 +109,10 @@ class ValidateMeshArnoldAttributes(pyblish.api.InstancePlugin, if not self.is_active(instance.data): return + if not cmds.pluginInfo("mtoa", query=True, loaded=True): + # Arnold attributes only exist if plug-in is loaded + return + invalid = self.get_invalid_attributes(instance, compute=True) if invalid: raise PublishValidationError( From 17f83a0eccb99a35ba44faaf876ad3604b342932 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 30 Aug 2023 14:17:16 +0200 Subject: [PATCH 206/327] Avoid iterating over `None` if `ai` attributes for some reason do not exist --- .../maya/plugins/publish/validate_mesh_arnold_attributes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_mesh_arnold_attributes.py b/openpype/hosts/maya/plugins/publish/validate_mesh_arnold_attributes.py index 59ba326d45..ddb8d2bf9d 100644 --- a/openpype/hosts/maya/plugins/publish/validate_mesh_arnold_attributes.py +++ b/openpype/hosts/maya/plugins/publish/validate_mesh_arnold_attributes.py @@ -51,7 +51,7 @@ class ValidateMeshArnoldAttributes(pyblish.api.InstancePlugin, tmp.append(transform) mesh = cmds.createNode("mesh", parent=transform) - for attr in cmds.listAttr(mesh, string="ai*"): + for attr in cmds.listAttr(mesh, string="ai*") or []: plug = "{}.{}".format(mesh, attr) try: defaults[attr] = get_attribute(plug) From 861b9849cb11b82ad70ef99806c1c0498ec30b1b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 30 Aug 2023 14:20:56 +0200 Subject: [PATCH 207/327] Ensure we only iterate `ai*` attributes that come from a plugin --- .../maya/plugins/publish/validate_mesh_arnold_attributes.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_mesh_arnold_attributes.py b/openpype/hosts/maya/plugins/publish/validate_mesh_arnold_attributes.py index ddb8d2bf9d..849d60653a 100644 --- a/openpype/hosts/maya/plugins/publish/validate_mesh_arnold_attributes.py +++ b/openpype/hosts/maya/plugins/publish/validate_mesh_arnold_attributes.py @@ -51,7 +51,10 @@ class ValidateMeshArnoldAttributes(pyblish.api.InstancePlugin, tmp.append(transform) mesh = cmds.createNode("mesh", parent=transform) - for attr in cmds.listAttr(mesh, string="ai*") or []: + arnold_attributes = cmds.listAttr(mesh, + string="ai*", + fromPlugin=True) or [] + for attr in arnold_attributes: plug = "{}.{}".format(mesh, attr) try: defaults[attr] = get_attribute(plug) From 3ee28bd1aa092f90e5b807dc63eea135eb913a8b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 30 Aug 2023 14:22:11 +0200 Subject: [PATCH 208/327] Avoid selection changes --- .../maya/plugins/publish/validate_mesh_arnold_attributes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_mesh_arnold_attributes.py b/openpype/hosts/maya/plugins/publish/validate_mesh_arnold_attributes.py index 849d60653a..d50e1a56c7 100644 --- a/openpype/hosts/maya/plugins/publish/validate_mesh_arnold_attributes.py +++ b/openpype/hosts/maya/plugins/publish/validate_mesh_arnold_attributes.py @@ -47,10 +47,10 @@ class ValidateMeshArnoldAttributes(pyblish.api.InstancePlugin, # Get default arnold attribute values for mesh type. defaults = {} with delete_after() as tmp: - transform = cmds.createNode("transform") + transform = cmds.createNode("transform", skipSelect=True) tmp.append(transform) - mesh = cmds.createNode("mesh", parent=transform) + mesh = cmds.createNode("mesh", parent=transform, skipSelect=True) arnold_attributes = cmds.listAttr(mesh, string="ai*", fromPlugin=True) or [] From 3b8df1ac2bad6df683ae4417e2b2e0b59944f729 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 30 Aug 2023 14:29:40 +0200 Subject: [PATCH 209/327] Cache arnold mesh attribute defaults so multiple "model" instance don't try and retrieve the default values every time. --- .../plugins/publish/validate_mesh_arnold_attributes.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/validate_mesh_arnold_attributes.py b/openpype/hosts/maya/plugins/publish/validate_mesh_arnold_attributes.py index d50e1a56c7..526ebbac99 100644 --- a/openpype/hosts/maya/plugins/publish/validate_mesh_arnold_attributes.py +++ b/openpype/hosts/maya/plugins/publish/validate_mesh_arnold_attributes.py @@ -36,6 +36,9 @@ class ValidateMeshArnoldAttributes(pyblish.api.InstancePlugin, optional = True + # cache (will be `dict` when cached) + arnold_mesh_defaults = None + @classmethod def apply_settings(cls, project_settings, system_settings): # todo: this should not be done this way @@ -44,6 +47,11 @@ class ValidateMeshArnoldAttributes(pyblish.api.InstancePlugin, @classmethod def get_default_attributes(cls): + + if cls.arnold_mesh_defaults is not None: + # Use from cache + return cls.arnold_mesh_defaults + # Get default arnold attribute values for mesh type. defaults = {} with delete_after() as tmp: @@ -61,6 +69,7 @@ class ValidateMeshArnoldAttributes(pyblish.api.InstancePlugin, except PublishValidationError: cls.log.debug("Ignoring arnold attribute: {}".format(attr)) + cls.arnold_mesh_defaults = defaults # assign cache return defaults @classmethod From 689da974c4d22161797862f2a9e77d8aeee65656 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 30 Aug 2023 14:42:46 +0200 Subject: [PATCH 210/327] Removed `apply_settings` override to fix plugin settings actually being applied This makes it so again that it's up to the studio admin to enable or disable the plug-in per project instead of whether the modeler's workfile just happened to have the current renderer set to arnold or not. Note that the plug-in is currently disabled by default in project settings. --- .../maya/plugins/publish/validate_mesh_arnold_attributes.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_mesh_arnold_attributes.py b/openpype/hosts/maya/plugins/publish/validate_mesh_arnold_attributes.py index 526ebbac99..bde78a98b8 100644 --- a/openpype/hosts/maya/plugins/publish/validate_mesh_arnold_attributes.py +++ b/openpype/hosts/maya/plugins/publish/validate_mesh_arnold_attributes.py @@ -39,12 +39,6 @@ class ValidateMeshArnoldAttributes(pyblish.api.InstancePlugin, # cache (will be `dict` when cached) arnold_mesh_defaults = None - @classmethod - def apply_settings(cls, project_settings, system_settings): - # todo: this should not be done this way - attr = "defaultRenderGlobals.currentRenderer" - cls.active = cmds.getAttr(attr).lower() == "arnold" - @classmethod def get_default_attributes(cls): From b83a40931385b0688cad416a7a998fb9b3f6f7c1 Mon Sep 17 00:00:00 2001 From: sjt-rvx <72554834+sjt-rvx@users.noreply.github.com> Date: Wed, 30 Aug 2023 13:12:03 +0000 Subject: [PATCH 211/327] have the addons loading respect a custom AYON_ADDONS_DIR (#5539) * have the addons loading respect a custom AYON_ADDONS_DIR * Update openpype/modules/base.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --------- Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/modules/base.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 9b3637c48a..84e213288c 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -373,10 +373,12 @@ def _load_ayon_addons(openpype_modules, modules_key, log): addons_info = _get_ayon_addons_information() if not addons_info: return v3_addons_to_skip - addons_dir = os.path.join( - appdirs.user_data_dir("AYON", "Ynput"), - "addons" - ) + addons_dir = os.environ.get("AYON_ADDONS_DIR") + if not addons_dir: + addons_dir = os.path.join( + appdirs.user_data_dir("AYON", "Ynput"), + "addons" + ) if not os.path.exists(addons_dir): log.warning("Addons directory does not exists. Path \"{}\"".format( addons_dir From 3c3438532018c6360b77a2b5c86959638e73d86f Mon Sep 17 00:00:00 2001 From: Mustafa Zarkash Date: Wed, 30 Aug 2023 16:35:09 +0300 Subject: [PATCH 212/327] Houdini: Improve VDB Selection (#5523) * improve sop selection * resolve hound conversations * resolve BigRoy's comments --- .../plugins/create/create_vbd_cache.py | 62 ++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/create/create_vbd_cache.py b/openpype/hosts/houdini/plugins/create/create_vbd_cache.py index c015cebd49..9c96e48e3a 100644 --- a/openpype/hosts/houdini/plugins/create/create_vbd_cache.py +++ b/openpype/hosts/houdini/plugins/create/create_vbd_cache.py @@ -33,7 +33,7 @@ class CreateVDBCache(plugin.HoudiniCreator): } if self.selected_nodes: - parms["soppath"] = self.selected_nodes[0].path() + parms["soppath"] = self.get_sop_node_path(self.selected_nodes[0]) instance_node.setParms(parms) @@ -42,3 +42,63 @@ class CreateVDBCache(plugin.HoudiniCreator): hou.ropNodeTypeCategory(), hou.sopNodeTypeCategory() ] + + def get_sop_node_path(self, selected_node): + """Get Sop Path of the selected node. + + 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) + if isinstance(selected_node, hou.SopNode): + self.log.debug( + "Valid SopNode selection, 'SOP Path' in ROP will" + " be set to '%s'.", selected_node.path() + ) + return 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": + + # Try to find output node. + sop_node = self.get_obj_output(selected_node) + if sop_node: + self.log.debug( + "Valid ObjNode selection, 'SOP Path' in ROP will " + "be set to the child path '%s'.", sop_node.path() + ) + return sop_node.path() + + self.log.debug( + "Selection isn't valid. 'SOP Path' in ROP will be empty." + ) + return "" + + def get_obj_output(self, obj_node): + """Try to find output node. + + If any output nodes are present, return the output node with + the minimum 'outputidx' + If no output nodes are present, return the node with display flag + If no nodes are present at all, return None + """ + + outputs = obj_node.subnetOutputs() + + # 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 has multiple output nodes + # return the one with the minimum 'outputidx' + else: + return min(outputs, + key=lambda node: node.evalParm('outputidx')) From a60c3d7ce304f648e13c6ab712a9a558b2186a65 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 30 Aug 2023 16:36:42 +0200 Subject: [PATCH 213/327] use correct git url in README (#5542) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 92f1cb62dc..ce98f845e6 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ development tools like [CMake](https://cmake.org/) and [Visual Studio](https://v #### Clone repository: ```sh -git clone --recurse-submodules git@github.com:Pypeclub/OpenPype.git +git clone --recurse-submodules git@github.com:ynput/OpenPype.git ``` #### To build OpenPype: From 74d612208ec4dc0e3a31891ce93dfca5a02d4d48 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 30 Aug 2023 17:47:03 +0200 Subject: [PATCH 214/327] AYON: Deadline expand userpaths in executables list (#5540) * expand userpaths set in executables list * Update logic for searching executable with expanduser --------- Co-authored-by: Petr Kalis --- .../deadline/repository/custom/plugins/Ayon/Ayon.py | 8 +++++++- .../repository/custom/plugins/GlobalJobPreLoad.py | 9 ++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.py b/openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.py index 1544acc2a4..a29acf9823 100644 --- a/openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.py +++ b/openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.py @@ -91,7 +91,13 @@ class AyonDeadlinePlugin(DeadlinePlugin): # clean '\ ' for MacOS pasting if platform.system().lower() == "darwin": exe_list = exe_list.replace("\\ ", " ") - exe = FileUtils.SearchFileList(exe_list) + + expanded_paths = [] + for path in exe_list.split(";"): + if path.startswith("~"): + path = os.path.expanduser(path) + expanded_paths.append(path) + exe = FileUtils.SearchFileList(";".join(expanded_paths)) if exe == "": self.FailRender( diff --git a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py index 5f7e1f1032..97875215ae 100644 --- a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py +++ b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py @@ -547,7 +547,14 @@ def get_ayon_executable(): # clean '\ ' for MacOS pasting if platform.system().lower() == "darwin": exe_list = exe_list.replace("\\ ", " ") - return exe_list + + # Expand user paths + expanded_paths = [] + for path in exe_list.split(";"): + if path.startswith("~"): + path = os.path.expanduser(path) + expanded_paths.append(path) + return ";".join(expanded_paths) def inject_render_job_id(deadlinePlugin): From 668bcb2d40d5708fc40c1e1a8465e36c94ffdef8 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 30 Aug 2023 22:28:43 +0300 Subject: [PATCH 215/327] BigRoy's Comments --- openpype/hosts/houdini/api/colorspace.py | 19 +++++++------------ openpype/pipeline/colorspace.py | 2 +- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/houdini/api/colorspace.py b/openpype/hosts/houdini/api/colorspace.py index 2662a968e2..fb0a724eb9 100644 --- a/openpype/hosts/houdini/api/colorspace.py +++ b/openpype/hosts/houdini/api/colorspace.py @@ -57,16 +57,11 @@ class ARenderProduct(object): def get_default_display_view_colorspace(): - """Get default display view colorspace. + """Get default display view colorspace name. """ - It's used for 'ociocolorspace' parm in OpneGL Node.""" - - data = get_color_management_preferences() - config_path = data.get("config") - display = data.get("display") - view = data.get("view") - - default_view_space = get_display_view_colorspace_name(config_path, - display, - view) - return default_view_space + prefs = get_color_management_preferences() + return get_display_view_colorspace_name( + config_path=prefs["config"], + display=prefs["display"], + view=prefs["view"] + ) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 37974f4a0b..3bb258e8f2 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -650,7 +650,7 @@ def get_display_view_colorspace_subprocess(config_path, display, view): "--view", view ] - log.info("Executing: {}".format(" ".join(args))) + log.debug("Executing: {}".format(" ".join(args))) process_kwargs = { "logger": log From 41babcaa85880b957a6409bf29d02da2291f91d2 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 30 Aug 2023 22:43:52 +0300 Subject: [PATCH 216/327] update doc strings --- openpype/hosts/houdini/api/colorspace.py | 4 +++- openpype/pipeline/colorspace.py | 4 ++-- openpype/scripts/ocio_wrapper.py | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/houdini/api/colorspace.py b/openpype/hosts/houdini/api/colorspace.py index fb0a724eb9..b1a4d5dcd5 100644 --- a/openpype/hosts/houdini/api/colorspace.py +++ b/openpype/hosts/houdini/api/colorspace.py @@ -57,7 +57,9 @@ class ARenderProduct(object): def get_default_display_view_colorspace(): - """Get default display view colorspace name. """ + """Returns the colorspace attribute of the default (display, view) pair. + + """ prefs = get_color_management_preferences() return get_display_view_colorspace_name( diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 3bb258e8f2..3dd33d0425 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -604,7 +604,7 @@ def _get_imageio_settings(project_settings, host_name): def get_display_view_colorspace_name(config_path, display, view): - """Return colorspace name for the given display and view. + """Returns the colorspace attribute of the (display, view) pair. Args: config_path (str): path string leading to config.ocio @@ -627,7 +627,7 @@ def get_display_view_colorspace_name(config_path, display, view): def get_display_view_colorspace_subprocess(config_path, display, view): - """get view colorspace name for the given display and view + """Returns the colorspace attribute of the (display, view) pair via subprocess. Args: diff --git a/openpype/scripts/ocio_wrapper.py b/openpype/scripts/ocio_wrapper.py index e491206ebb..cae6e6975b 100644 --- a/openpype/scripts/ocio_wrapper.py +++ b/openpype/scripts/ocio_wrapper.py @@ -175,7 +175,7 @@ def _get_views_data(config_path): def _get_display_view_colorspace_name(config_path, display, view): - """get view colorspace name for the given display and view. + """Returns the colorspace attribute of the (display, view) pair. Args: config_path (str): path string leading to config.ocio From 6c5039f7075ce59b2911a680d41ffae5a59dca09 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 30 Aug 2023 22:46:07 +0300 Subject: [PATCH 217/327] BigRoy's Comment --- openpype/hosts/houdini/api/colorspace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/api/colorspace.py b/openpype/hosts/houdini/api/colorspace.py index b1a4d5dcd5..cc40b9df1c 100644 --- a/openpype/hosts/houdini/api/colorspace.py +++ b/openpype/hosts/houdini/api/colorspace.py @@ -59,7 +59,7 @@ class ARenderProduct(object): def get_default_display_view_colorspace(): """Returns the colorspace attribute of the default (display, view) pair. - """ + It's used for 'ociocolorspace' parm in OpenGL Node.""" prefs = get_color_management_preferences() return get_display_view_colorspace_name( From 9e19f2f8ca1e7e848a7cebde4432df1fde3d3897 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 30 Aug 2023 22:09:33 +0200 Subject: [PATCH 218/327] implemented multiselection EnumDef --- openpype/lib/attribute_definitions.py | 30 +++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/openpype/lib/attribute_definitions.py b/openpype/lib/attribute_definitions.py index 6054d2a92a..0af701a93a 100644 --- a/openpype/lib/attribute_definitions.py +++ b/openpype/lib/attribute_definitions.py @@ -434,7 +434,9 @@ class EnumDef(AbstractAttrDef): type = "enum" - def __init__(self, key, items, default=None, **kwargs): + def __init__( + self, key, items, default=None, multiselection=False, **kwargs + ): if not items: raise ValueError(( "Empty 'items' value. {} must have" @@ -443,7 +445,10 @@ class EnumDef(AbstractAttrDef): items = self.prepare_enum_items(items) item_values = [item["value"] for item in items] - if default not in item_values: + if multiselection and default is None: + default = [] + + if not multiselection and default not in item_values: for value in item_values: default = value break @@ -452,21 +457,34 @@ class EnumDef(AbstractAttrDef): self.items = items self._item_values = set(item_values) + self.multiselection = multiselection def __eq__(self, other): if not super(EnumDef, self).__eq__(other): return False - return self.items == other.items + return ( + self.items == other.items + and self.multiselection == other.multiselection + ) def convert_value(self, value): - if value in self._item_values: - return value - return self.default + if not self.multiselection: + if value in self._item_values: + return value + return self.default + + if value is None: + return copy.deepcopy(self.default) + new_value = set(value) + rem = new_value - self._item_values + return list(new_value - rem) + def serialize(self): data = super(EnumDef, self).serialize() data["items"] = copy.deepcopy(self.items) + data["multiselection"] = self.multiselection return data @staticmethod From ebec7236aae738a2ffd9fd2738b9f402ead42c2e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 30 Aug 2023 22:09:51 +0200 Subject: [PATCH 219/327] implemented multiselection combobox --- openpype/tools/utils/__init__.py | 3 + .../tools/utils/multiselection_combobox.py | 383 ++++++++++++++++++ 2 files changed, 386 insertions(+) create mode 100644 openpype/tools/utils/multiselection_combobox.py diff --git a/openpype/tools/utils/__init__.py b/openpype/tools/utils/__init__.py index f35bfaee70..d343353112 100644 --- a/openpype/tools/utils/__init__.py +++ b/openpype/tools/utils/__init__.py @@ -38,6 +38,7 @@ from .models import ( from .overlay_messages import ( MessageOverlayObject, ) +from .multiselection_combobox import MultiSelectionComboBox __all__ = ( @@ -78,4 +79,6 @@ __all__ = ( "RecursiveSortFilterProxyModel", "MessageOverlayObject", + + "MultiSelectionComboBox", ) diff --git a/openpype/tools/utils/multiselection_combobox.py b/openpype/tools/utils/multiselection_combobox.py new file mode 100644 index 0000000000..27576a449a --- /dev/null +++ b/openpype/tools/utils/multiselection_combobox.py @@ -0,0 +1,383 @@ +from qtpy import QtCore, QtGui, QtWidgets + +from .lib import ( + checkstate_int_to_enum, + checkstate_enum_to_int, +) +from .constants import ( + CHECKED_INT, + UNCHECKED_INT, + ITEM_IS_USER_TRISTATE, +) + + +class ComboItemDelegate(QtWidgets.QStyledItemDelegate): + """ + Helper styled delegate (mostly based on existing private Qt's + delegate used by the QtWidgets.QComboBox). Used to style the popup like a + list view (e.g windows style). + """ + + def paint(self, painter, option, index): + option = QtWidgets.QStyleOptionViewItem(option) + option.showDecorationSelected = True + + # option.state &= ( + # ~QtWidgets.QStyle.State_HasFocus + # & ~QtWidgets.QStyle.State_MouseOver + # ) + super(ComboItemDelegate, self).paint(painter, option, index) + + +class MultiSelectionComboBox(QtWidgets.QComboBox): + value_changed = QtCore.Signal() + focused_in = QtCore.Signal() + + ignored_keys = { + QtCore.Qt.Key_Up, + QtCore.Qt.Key_Down, + QtCore.Qt.Key_PageDown, + QtCore.Qt.Key_PageUp, + QtCore.Qt.Key_Home, + QtCore.Qt.Key_End, + } + + top_bottom_padding = 2 + left_right_padding = 3 + left_offset = 4 + top_bottom_margins = 2 + item_spacing = 5 + + item_bg_color = QtGui.QColor("#31424e") + + def __init__( + self, parent=None, placeholder="", separator=", ", **kwargs + ): + super(MultiSelectionComboBox, self).__init__(parent=parent, **kwargs) + self.setObjectName("MultiSelectionComboBox") + self.setFocusPolicy(QtCore.Qt.StrongFocus) + + self._popup_is_shown = False + self._block_mouse_release_timer = QtCore.QTimer(self, singleShot=True) + self._initial_mouse_pos = None + self._separator = separator + self._placeholder_text = placeholder + delegate = ComboItemDelegate(self) + self.setItemDelegate(delegate) + + self._lines = {} + self._item_height = None + self._custom_text = None + self._delegate = delegate + + def get_placeholder_text(self): + return self._placeholder_text + + def set_placeholder_text(self, text): + self._placeholder_text = text + + def set_custom_text(self, text): + self._custom_text = text + self.update() + self.updateGeometry() + + def focusInEvent(self, event): + self.focused_in.emit() + return super(MultiSelectionComboBox, self).focusInEvent(event) + + def mousePressEvent(self, event): + """Reimplemented.""" + self._popup_is_shown = False + super(MultiSelectionComboBox, self).mousePressEvent(event) + if self._popup_is_shown: + self._initial_mouse_pos = self.mapToGlobal(event.pos()) + self._block_mouse_release_timer.start( + QtWidgets.QApplication.doubleClickInterval() + ) + + def showPopup(self): + """Reimplemented.""" + super(MultiSelectionComboBox, self).showPopup() + view = self.view() + view.installEventFilter(self) + view.viewport().installEventFilter(self) + self._popup_is_shown = True + + def hidePopup(self): + """Reimplemented.""" + self.view().removeEventFilter(self) + self.view().viewport().removeEventFilter(self) + self._popup_is_shown = False + self._initial_mouse_pos = None + super(MultiSelectionComboBox, self).hidePopup() + self.view().clearFocus() + + def _event_popup_shown(self, obj, event): + if not self._popup_is_shown: + return + + current_index = self.view().currentIndex() + model = self.model() + + if event.type() == QtCore.QEvent.MouseMove: + if ( + self.view().isVisible() + and self._initial_mouse_pos is not None + and self._block_mouse_release_timer.isActive() + ): + diff = obj.mapToGlobal(event.pos()) - self._initial_mouse_pos + if diff.manhattanLength() > 9: + self._block_mouse_release_timer.stop() + return + + index_flags = current_index.flags() + state = checkstate_int_to_enum( + current_index.data(QtCore.Qt.CheckStateRole) + ) + new_state = None + + if event.type() == QtCore.QEvent.MouseButtonRelease: + if ( + self._block_mouse_release_timer.isActive() + or not current_index.isValid() + or not self.view().isVisible() + or not self.view().rect().contains(event.pos()) + or not index_flags & QtCore.Qt.ItemIsSelectable + or not index_flags & QtCore.Qt.ItemIsEnabled + or not index_flags & QtCore.Qt.ItemIsUserCheckable + ): + return + + if state == QtCore.Qt.Unchecked: + new_state = CHECKED_INT + else: + new_state = UNCHECKED_INT + + elif event.type() == QtCore.QEvent.KeyPress: + # TODO: handle QtCore.Qt.Key_Enter, Key_Return? + if event.key() == QtCore.Qt.Key_Space: + if ( + index_flags & QtCore.Qt.ItemIsUserCheckable + and index_flags & ITEM_IS_USER_TRISTATE + ): + new_state = (checkstate_enum_to_int(state) + 1) % 3 + + elif index_flags & QtCore.Qt.ItemIsUserCheckable: + # toggle the current items check state + if state != QtCore.Qt.Checked: + new_state = CHECKED_INT + else: + new_state = UNCHECKED_INT + + if new_state is not None: + model.setData(current_index, new_state, QtCore.Qt.CheckStateRole) + self.view().update(current_index) + self._update_size_hint() + self.value_changed.emit() + return True + + def eventFilter(self, obj, event): + """Reimplemented.""" + result = self._event_popup_shown(obj, event) + if result is not None: + return result + + return super(MultiSelectionComboBox, self).eventFilter(obj, event) + + def addItem(self, *args, **kwargs): + idx = self.count() + super(MultiSelectionComboBox, self).addItem(*args, **kwargs) + self.model().item(idx).setCheckable(True) + + def paintEvent(self, event): + """Reimplemented.""" + painter = QtWidgets.QStylePainter(self) + option = QtWidgets.QStyleOptionComboBox() + self.initStyleOption(option) + painter.drawComplexControl(QtWidgets.QStyle.CC_ComboBox, option) + + items = self.checked_items_text() + # draw the icon and text + draw_text = True + combotext = None + if self._custom_text is not None: + combotext = self._custom_text + elif not items: + combotext = self._placeholder_text + else: + draw_text = False + if draw_text: + option.currentText = combotext + option.palette.setCurrentColorGroup(QtGui.QPalette.Disabled) + painter.drawControl(QtWidgets.QStyle.CE_ComboBoxLabel, option) + return + + font_metricts = self.fontMetrics() + + if self._item_height is None: + self.updateGeometry() + self.update() + return + + for line, items in self._lines.items(): + top_y = ( + option.rect.top() + + (line * self._item_height) + + self.top_bottom_margins + ) + left_x = option.rect.left() + self.left_offset + for item in items: + label_rect = font_metricts.boundingRect(item) + label_height = label_rect.height() + + label_rect.moveTop(top_y) + label_rect.moveLeft(left_x) + label_rect.setHeight(self._item_height) + label_rect.setWidth( + label_rect.width() + self.left_right_padding + ) + + bg_rect = QtCore.QRectF(label_rect) + bg_rect.setWidth( + label_rect.width() + self.left_right_padding + ) + left_x = bg_rect.right() + self.item_spacing + + label_rect.moveLeft(label_rect.x() + self.left_right_padding) + + bg_rect.setHeight(label_height + (2 * self.top_bottom_padding)) + bg_rect.moveTop(bg_rect.top() + self.top_bottom_margins) + + path = QtGui.QPainterPath() + path.addRoundedRect(bg_rect, 5, 5) + + painter.fillPath(path, self.item_bg_color) + + painter.drawText( + label_rect, + QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, + item + ) + + def resizeEvent(self, *args, **kwargs): + super(MultiSelectionComboBox, self).resizeEvent(*args, **kwargs) + self._update_size_hint() + + def _update_size_hint(self): + if self._custom_text is not None: + self.update() + return + self._lines = {} + + items = self.checked_items_text() + if not items: + self.update() + return + + option = QtWidgets.QStyleOptionComboBox() + self.initStyleOption(option) + btn_rect = self.style().subControlRect( + QtWidgets.QStyle.CC_ComboBox, + option, + QtWidgets.QStyle.SC_ComboBoxArrow + ) + total_width = option.rect.width() - btn_rect.width() + + line = 0 + self._lines = {line: []} + + font_metricts = self.fontMetrics() + default_left_x = 0 + self.left_offset + left_x = int(default_left_x) + for item in items: + rect = font_metricts.boundingRect(item) + width = rect.width() + (2 * self.left_right_padding) + right_x = left_x + width + if right_x > total_width: + left_x = int(default_left_x) + if self._lines.get(line): + line += 1 + self._lines[line] = [item] + left_x += width + else: + self._lines[line] = [item] + line += 1 + else: + if line in self._lines: + self._lines[line].append(item) + else: + self._lines[line] = [item] + left_x = left_x + width + self.item_spacing + + self.update() + self.updateGeometry() + + def sizeHint(self): + value = super(MultiSelectionComboBox, self).sizeHint() + lines = 1 + if self._custom_text is None: + lines = len(self._lines) + if lines == 0: + lines = 1 + + if self._item_height is None: + self._item_height = ( + self.fontMetrics().height() + + (2 * self.top_bottom_padding) + + (2 * self.top_bottom_margins) + ) + value.setHeight( + (lines * self._item_height) + + (2 * self.top_bottom_margins) + ) + return value + + def setItemCheckState(self, index, state): + self.setItemData(index, state, QtCore.Qt.CheckStateRole) + + def set_value(self, values): + for idx in range(self.count()): + value = self.itemData(idx, role=QtCore.Qt.UserRole) + if value in values: + check_state = CHECKED_INT + else: + check_state = UNCHECKED_INT + self.setItemData(idx, check_state, QtCore.Qt.CheckStateRole) + self._update_size_hint() + + def value(self): + items = list() + for idx in range(self.count()): + state = checkstate_int_to_enum( + self.itemData(idx, role=QtCore.Qt.CheckStateRole) + ) + if state == QtCore.Qt.Checked: + items.append( + self.itemData(idx, role=QtCore.Qt.UserRole) + ) + return items + + def checked_items_text(self): + items = list() + for idx in range(self.count()): + state = checkstate_int_to_enum( + self.itemData(idx, role=QtCore.Qt.CheckStateRole) + ) + if state == QtCore.Qt.Checked: + items.append(self.itemText(idx)) + return items + + def wheelEvent(self, event): + event.ignore() + + def keyPressEvent(self, event): + if ( + event.key() == QtCore.Qt.Key_Down + and event.modifiers() & QtCore.Qt.AltModifier + ): + return self.showPopup() + + if event.key() in self.ignored_keys: + return event.ignore() + + return super(MultiSelectionComboBox, self).keyPressEvent(event) From 2d04efba93c50e6832059ae92d49bee9aab641fb Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 30 Aug 2023 22:10:14 +0200 Subject: [PATCH 220/327] implemented multuselection EnumAttrDef widget --- openpype/tools/attribute_defs/widgets.py | 55 +++++++++++++++++++----- 1 file changed, 45 insertions(+), 10 deletions(-) diff --git a/openpype/tools/attribute_defs/widgets.py b/openpype/tools/attribute_defs/widgets.py index 7967416e9f..e08ca17225 100644 --- a/openpype/tools/attribute_defs/widgets.py +++ b/openpype/tools/attribute_defs/widgets.py @@ -19,6 +19,7 @@ from openpype.tools.utils import ( CustomTextComboBox, FocusSpinBox, FocusDoubleSpinBox, + MultiSelectionComboBox, ) from openpype.widgets.nice_checkbox import NiceCheckbox @@ -412,10 +413,19 @@ class EnumAttrWidget(_BaseAttrDefWidget): self._multivalue = False super(EnumAttrWidget, self).__init__(*args, **kwargs) + @property + def multiselection(self): + return self.attr_def.multiselection + def _ui_init(self): - input_widget = CustomTextComboBox(self) - combo_delegate = QtWidgets.QStyledItemDelegate(input_widget) - input_widget.setItemDelegate(combo_delegate) + if self.multiselection: + input_widget = MultiSelectionComboBox(self) + + else: + input_widget = CustomTextComboBox(self) + combo_delegate = QtWidgets.QStyledItemDelegate(input_widget) + input_widget.setItemDelegate(combo_delegate) + self._combo_delegate = combo_delegate if self.attr_def.tooltip: input_widget.setToolTip(self.attr_def.tooltip) @@ -427,9 +437,11 @@ class EnumAttrWidget(_BaseAttrDefWidget): if idx >= 0: input_widget.setCurrentIndex(idx) - input_widget.currentIndexChanged.connect(self._on_value_change) + if self.multiselection: + input_widget.value_changed.connect(self._on_value_change) + else: + input_widget.currentIndexChanged.connect(self._on_value_change) - self._combo_delegate = combo_delegate self._input_widget = input_widget self.main_layout.addWidget(input_widget, 0) @@ -442,17 +454,40 @@ class EnumAttrWidget(_BaseAttrDefWidget): self.value_changed.emit(new_value, self.attr_def.id) def current_value(self): + if self.multiselection: + return self._input_widget.value() idx = self._input_widget.currentIndex() return self._input_widget.itemData(idx) def set_value(self, value, multivalue=False): if multivalue: - set_value = set(value) - if len(set_value) == 1: - multivalue = False - value = tuple(set_value)[0] + if self.multiselection: + _value = None + are_same = True + for v in value: + _v = set(v) + if _value is None: + _value = _v + elif not are_same or _value != _v: + _value |= _v + are_same = False + if _value is None: + _value = [] + else: + value = list(_value) + multivalue = not are_same + else: + set_value = set(value) + if len(set_value) == 1: + multivalue = False + value = tuple(set_value)[0] - if not multivalue: + if self.multiselection: + self._input_widget.blockSignals(True) + self._input_widget.set_value(value) + self._input_widget.blockSignals(False) + + elif not multivalue: idx = self._input_widget.findData(value) cur_idx = self._input_widget.currentIndex() if idx != cur_idx and idx >= 0: From 7ebec6b8bcd737b6d393d9a50b9f4744a2e841b0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 30 Aug 2023 22:55:44 +0200 Subject: [PATCH 221/327] simplified few lines --- openpype/lib/attribute_definitions.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/openpype/lib/attribute_definitions.py b/openpype/lib/attribute_definitions.py index 0af701a93a..44aa8dee48 100644 --- a/openpype/lib/attribute_definitions.py +++ b/openpype/lib/attribute_definitions.py @@ -445,18 +445,19 @@ class EnumDef(AbstractAttrDef): items = self.prepare_enum_items(items) item_values = [item["value"] for item in items] - if multiselection and default is None: - default = [] + item_values_set = set(item_values) + if multiselection: + if default is None: + default = [] + default = list(item_values_set.intersection(default)) - if not multiselection and default not in item_values: - for value in item_values: - default = value - break + elif default not in item_values: + default = next(iter(item_values), None) super(EnumDef, self).__init__(key, default=default, **kwargs) self.items = items - self._item_values = set(item_values) + self._item_values = item_values_set self.multiselection = multiselection def __eq__(self, other): @@ -476,9 +477,7 @@ class EnumDef(AbstractAttrDef): if value is None: return copy.deepcopy(self.default) - new_value = set(value) - rem = new_value - self._item_values - return list(new_value - rem) + return list(self._item_values.intersection(value)) def serialize(self): From 8cf4c9c6515c7eb6d9db92c57d48a21acf3821f9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 30 Aug 2023 23:03:45 +0200 Subject: [PATCH 222/327] move multiselection multivalue handling to separated method --- openpype/tools/attribute_defs/widgets.py | 28 ++++++++++++------------ 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/openpype/tools/attribute_defs/widgets.py b/openpype/tools/attribute_defs/widgets.py index e08ca17225..adb29dcbd3 100644 --- a/openpype/tools/attribute_defs/widgets.py +++ b/openpype/tools/attribute_defs/widgets.py @@ -459,23 +459,23 @@ class EnumAttrWidget(_BaseAttrDefWidget): idx = self._input_widget.currentIndex() return self._input_widget.itemData(idx) + def _multiselection_multivalue_prep(self, values): + final = None + are_same = True + for value in values: + value = set(value) + if final is None: + final = value + elif not are_same or final != value: + final |= value + are_same = False + return list(final), not are_same + def set_value(self, value, multivalue=False): if multivalue: if self.multiselection: - _value = None - are_same = True - for v in value: - _v = set(v) - if _value is None: - _value = _v - elif not are_same or _value != _v: - _value |= _v - are_same = False - if _value is None: - _value = [] - else: - value = list(_value) - multivalue = not are_same + value, multivalue = self._multiselection_multivalue_prep( + value) else: set_value = set(value) if len(set_value) == 1: From 5b4bdeb6259dc5bd69ec773ba717a1efad17a695 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 30 Aug 2023 23:14:54 +0200 Subject: [PATCH 223/327] avoid unnecessary negative checks --- openpype/tools/attribute_defs/widgets.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/tools/attribute_defs/widgets.py b/openpype/tools/attribute_defs/widgets.py index adb29dcbd3..d9c55f4a64 100644 --- a/openpype/tools/attribute_defs/widgets.py +++ b/openpype/tools/attribute_defs/widgets.py @@ -461,15 +461,15 @@ class EnumAttrWidget(_BaseAttrDefWidget): def _multiselection_multivalue_prep(self, values): final = None - are_same = True + multivalue = False for value in values: value = set(value) if final is None: final = value - elif not are_same or final != value: + elif multivalue or final != value: final |= value - are_same = False - return list(final), not are_same + multivalue = True + return list(final), multivalue def set_value(self, value, multivalue=False): if multivalue: From 055e8250007731c3ac516ae7dbee7736629d393b Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 31 Aug 2023 15:41:33 +0800 Subject: [PATCH 224/327] bigroy comment on the camera setup on validation --- openpype/hosts/max/plugins/publish/collect_render.py | 3 +++ .../max/plugins/publish/validate_viewport_camera.py | 12 +++++------- .../deadline/plugins/publish/submit_max_deadline.py | 8 ++++++-- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index db5c84fad9..7df71799d3 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -34,6 +34,9 @@ class CollectRender(pyblish.api.InstancePlugin): aovs = RenderProducts().get_aovs(instance.name) files_by_aov.update(aovs) + camera = rt.viewport.GetCamera() + instance.data["cameras"] = [camera.name] or None + if "expectedFiles" not in instance.data: instance.data["expectedFiles"] = list() instance.data["files"] = list() diff --git a/openpype/hosts/max/plugins/publish/validate_viewport_camera.py b/openpype/hosts/max/plugins/publish/validate_viewport_camera.py index b35ba482a9..533b55f969 100644 --- a/openpype/hosts/max/plugins/publish/validate_viewport_camera.py +++ b/openpype/hosts/max/plugins/publish/validate_viewport_camera.py @@ -13,8 +13,7 @@ class ValidateViewportCamera(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validates Viewport Camera - Check if the renderable camera in scene used as viewport - camera for rendering + Check if the renderable camera used for rendering """ order = pyblish.api.ValidatorOrder @@ -27,11 +26,9 @@ class ValidateViewportCamera(pyblish.api.InstancePlugin, def process(self, instance): if not self.is_active(instance.data): return - cameras_in_scene = [c for c in rt.Objects - if rt.classOf(c) in rt.Camera.Classes] - if rt.viewport.getCamera() not in cameras_in_scene: + if not instance.data["cameras"]: raise PublishValidationError( - "Cameras in Scene not used as viewport camera" + "No renderable Camera found in scene." ) @classmethod @@ -39,10 +36,11 @@ class ValidateViewportCamera(pyblish.api.InstancePlugin, rt.viewport.setType(rt.Name("view_camera")) camera = rt.viewport.GetCamera() - cls.log.info(f"Camera {camera} set as viewport camera") + cls.log.info(f"Camera {camera} set as renderable camera") renderer_class = get_current_renderer() renderer = str(renderer_class).split(":")[0] if renderer == "Arnold": arv = rt.MAXToAOps.ArnoldRenderView() arv.setOption("Camera", str(camera)) arv.close() + instance.data["cameras"] = [camera.name] diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index d8725e853c..6b42270d05 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -31,6 +31,7 @@ class MaxPluginInfo(object): Version = attr.ib(default=None) # Mandatory for Deadline SaveFile = attr.ib(default=True) IgnoreInputs = attr.ib(default=True) + Camera = attr.ib(default=None) class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, @@ -154,7 +155,8 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, SceneFile=self.scene_path, Version=instance.data["maxversion"], SaveFile=True, - IgnoreInputs=True + IgnoreInputs=True, + Camera=instance.data["cameras"][0] ) plugin_payload = attr.asdict(plugin_info) @@ -238,7 +240,9 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, if renderer == "Redshift_Renderer": plugin_data["redshift_SeparateAovFiles"] = instance.data.get( "separateAovFiles") - + if instance.data["cameras"]: + plugin_info["Camera0"] = None + plugin_info["Camera1"] = instance.data["cameras"][0] self.log.debug("plugin data:{}".format(plugin_data)) plugin_info.update(plugin_data) From 72d7a9191baca344498f6c686c44adaf0d570f26 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 31 Aug 2023 16:00:22 +0800 Subject: [PATCH 225/327] bug fix the camera instance data collection --- openpype/hosts/max/plugins/publish/collect_render.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 7df71799d3..e7730369b6 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -35,7 +35,9 @@ class CollectRender(pyblish.api.InstancePlugin): files_by_aov.update(aovs) camera = rt.viewport.GetCamera() - instance.data["cameras"] = [camera.name] or None + instance.data["cameras"] = None + if camera: + instance.data["cameras"] = [camera.name] if "expectedFiles" not in instance.data: instance.data["expectedFiles"] = list() From b9fb4ce9e9669cfd6773fde30139243e77981a3c Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 31 Aug 2023 16:05:02 +0800 Subject: [PATCH 226/327] rename the validator --- .../publish/validate_renderable_camera.py | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 openpype/hosts/max/plugins/publish/validate_renderable_camera.py diff --git a/openpype/hosts/max/plugins/publish/validate_renderable_camera.py b/openpype/hosts/max/plugins/publish/validate_renderable_camera.py new file mode 100644 index 0000000000..61321661b5 --- /dev/null +++ b/openpype/hosts/max/plugins/publish/validate_renderable_camera.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +import pyblish.api +from openpype.pipeline import ( + PublishValidationError, + OptionalPyblishPluginMixin) +from openpype.pipeline.publish import RepairAction +from openpype.hosts.max.api.lib import get_current_renderer + +from pymxs import runtime as rt + + +class ValidateRenderableCamera(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): + """Validates Renderable Camera + + Check if the renderable camera used for rendering + """ + + order = pyblish.api.ValidatorOrder + families = ["maxrender"] + hosts = ["max"] + label = "Renderable Camera" + optional = True + actions = [RepairAction] + + def process(self, instance): + if not self.is_active(instance.data): + return + if not instance.data["cameras"]: + raise PublishValidationError( + "No renderable Camera found in scene." + ) + + @classmethod + def repair(cls, instance): + + rt.viewport.setType(rt.Name("view_camera")) + camera = rt.viewport.GetCamera() + cls.log.info(f"Camera {camera} set as renderable camera") + renderer_class = get_current_renderer() + renderer = str(renderer_class).split(":")[0] + if renderer == "Arnold": + arv = rt.MAXToAOps.ArnoldRenderView() + arv.setOption("Camera", str(camera)) + arv.close() + instance.data["cameras"] = [camera.name] From 4e88f705ee67af7ba1c0a6e920288759be006fd5 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 31 Aug 2023 16:12:36 +0800 Subject: [PATCH 227/327] remove the duplicated validator --- .../publish/validate_viewport_camera.py | 46 ------------------- 1 file changed, 46 deletions(-) delete mode 100644 openpype/hosts/max/plugins/publish/validate_viewport_camera.py diff --git a/openpype/hosts/max/plugins/publish/validate_viewport_camera.py b/openpype/hosts/max/plugins/publish/validate_viewport_camera.py deleted file mode 100644 index 533b55f969..0000000000 --- a/openpype/hosts/max/plugins/publish/validate_viewport_camera.py +++ /dev/null @@ -1,46 +0,0 @@ -# -*- coding: utf-8 -*- -import pyblish.api -from openpype.pipeline import ( - PublishValidationError, - OptionalPyblishPluginMixin) -from openpype.pipeline.publish import RepairAction -from openpype.hosts.max.api.lib import get_current_renderer - -from pymxs import runtime as rt - - -class ValidateViewportCamera(pyblish.api.InstancePlugin, - OptionalPyblishPluginMixin): - """Validates Viewport Camera - - Check if the renderable camera used for rendering - """ - - order = pyblish.api.ValidatorOrder - families = ["maxrender"] - hosts = ["max"] - label = "Viewport Camera" - optional = True - actions = [RepairAction] - - def process(self, instance): - if not self.is_active(instance.data): - return - if not instance.data["cameras"]: - raise PublishValidationError( - "No renderable Camera found in scene." - ) - - @classmethod - def repair(cls, instance): - - rt.viewport.setType(rt.Name("view_camera")) - camera = rt.viewport.GetCamera() - cls.log.info(f"Camera {camera} set as renderable camera") - renderer_class = get_current_renderer() - renderer = str(renderer_class).split(":")[0] - if renderer == "Arnold": - arv = rt.MAXToAOps.ArnoldRenderView() - arv.setOption("Camera", str(camera)) - arv.close() - instance.data["cameras"] = [camera.name] From 5685e2a1681d2ea74ac481bfa31de6f0a370d55f Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 31 Aug 2023 16:48:52 +0800 Subject: [PATCH 228/327] introduce imprint function for correct version in hda loader --- openpype/hosts/houdini/plugins/load/load_hda.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/houdini/plugins/load/load_hda.py b/openpype/hosts/houdini/plugins/load/load_hda.py index 57edc341a3..9630716253 100644 --- a/openpype/hosts/houdini/plugins/load/load_hda.py +++ b/openpype/hosts/houdini/plugins/load/load_hda.py @@ -59,6 +59,9 @@ class HdaLoader(load.LoaderPlugin): def_paths = [d.libraryFilePath() for d in defs] new = def_paths.index(file_path) defs[new].setIsPreferred(True) + hda_node.setParms({ + "representation": str(representation["_id"]) + }) def remove(self, container): node = container["node"] From f6c2be8b2a57559418077a68054e9ae3dfdbd76e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 31 Aug 2023 11:15:37 +0200 Subject: [PATCH 229/327] force repaint --- openpype/tools/utils/multiselection_combobox.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/tools/utils/multiselection_combobox.py b/openpype/tools/utils/multiselection_combobox.py index 27576a449a..13b396a059 100644 --- a/openpype/tools/utils/multiselection_combobox.py +++ b/openpype/tools/utils/multiselection_combobox.py @@ -266,12 +266,14 @@ class MultiSelectionComboBox(QtWidgets.QComboBox): def _update_size_hint(self): if self._custom_text is not None: self.update() + self.repaint() return self._lines = {} items = self.checked_items_text() if not items: self.update() + self.repaint() return option = QtWidgets.QStyleOptionComboBox() @@ -311,6 +313,7 @@ class MultiSelectionComboBox(QtWidgets.QComboBox): self.update() self.updateGeometry() + self.repaint() def sizeHint(self): value = super(MultiSelectionComboBox, self).sizeHint() From b412e221646b56b57a911f9bab312756d360f640 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 31 Aug 2023 11:17:25 +0200 Subject: [PATCH 230/327] remove multiselection combobox from settings --- .../tools/settings/settings/item_widgets.py | 2 +- .../settings/multiselection_combobox.py | 356 ------------------ 2 files changed, 1 insertion(+), 357 deletions(-) delete mode 100644 openpype/tools/settings/settings/multiselection_combobox.py diff --git a/openpype/tools/settings/settings/item_widgets.py b/openpype/tools/settings/settings/item_widgets.py index 117eca7d6b..2fd13cbbd8 100644 --- a/openpype/tools/settings/settings/item_widgets.py +++ b/openpype/tools/settings/settings/item_widgets.py @@ -4,6 +4,7 @@ from qtpy import QtWidgets, QtCore, QtGui from openpype.widgets.sliders import NiceSlider from openpype.tools.settings import CHILD_OFFSET +from openpype.tools.utils import MultiSelectionComboBox from openpype.settings.entities.exceptions import BaseInvalidValue from .widgets import ( @@ -15,7 +16,6 @@ from .widgets import ( SettingsNiceCheckbox, SettingsLineEdit ) -from .multiselection_combobox import MultiSelectionComboBox from .wrapper_widgets import ( WrapperWidget, CollapsibleWrapper, diff --git a/openpype/tools/settings/settings/multiselection_combobox.py b/openpype/tools/settings/settings/multiselection_combobox.py deleted file mode 100644 index d64fc83745..0000000000 --- a/openpype/tools/settings/settings/multiselection_combobox.py +++ /dev/null @@ -1,356 +0,0 @@ -from qtpy import QtCore, QtGui, QtWidgets -from openpype.tools.utils.lib import ( - checkstate_int_to_enum, - checkstate_enum_to_int, -) -from openpype.tools.utils.constants import ( - CHECKED_INT, - UNCHECKED_INT, - ITEM_IS_USER_TRISTATE, -) - - -class ComboItemDelegate(QtWidgets.QStyledItemDelegate): - """ - Helper styled delegate (mostly based on existing private Qt's - delegate used by the QtWidgets.QComboBox). Used to style the popup like a - list view (e.g windows style). - """ - - def paint(self, painter, option, index): - option = QtWidgets.QStyleOptionViewItem(option) - option.showDecorationSelected = True - - # option.state &= ( - # ~QtWidgets.QStyle.State_HasFocus - # & ~QtWidgets.QStyle.State_MouseOver - # ) - super(ComboItemDelegate, self).paint(painter, option, index) - - -class MultiSelectionComboBox(QtWidgets.QComboBox): - value_changed = QtCore.Signal() - focused_in = QtCore.Signal() - - ignored_keys = { - QtCore.Qt.Key_Up, - QtCore.Qt.Key_Down, - QtCore.Qt.Key_PageDown, - QtCore.Qt.Key_PageUp, - QtCore.Qt.Key_Home, - QtCore.Qt.Key_End, - } - - top_bottom_padding = 2 - left_right_padding = 3 - left_offset = 4 - top_bottom_margins = 2 - item_spacing = 5 - - item_bg_color = QtGui.QColor("#31424e") - - def __init__( - self, parent=None, placeholder="", separator=", ", **kwargs - ): - super(MultiSelectionComboBox, self).__init__(parent=parent, **kwargs) - self.setObjectName("MultiSelectionComboBox") - self.setFocusPolicy(QtCore.Qt.StrongFocus) - - self._popup_is_shown = False - self._block_mouse_release_timer = QtCore.QTimer(self, singleShot=True) - self._initial_mouse_pos = None - self._separator = separator - self.placeholder_text = placeholder - self.delegate = ComboItemDelegate(self) - self.setItemDelegate(self.delegate) - - self.lines = {} - self.item_height = None - - def focusInEvent(self, event): - self.focused_in.emit() - return super(MultiSelectionComboBox, self).focusInEvent(event) - - def mousePressEvent(self, event): - """Reimplemented.""" - self._popup_is_shown = False - super(MultiSelectionComboBox, self).mousePressEvent(event) - if self._popup_is_shown: - self._initial_mouse_pos = self.mapToGlobal(event.pos()) - self._block_mouse_release_timer.start( - QtWidgets.QApplication.doubleClickInterval() - ) - - def showPopup(self): - """Reimplemented.""" - super(MultiSelectionComboBox, self).showPopup() - view = self.view() - view.installEventFilter(self) - view.viewport().installEventFilter(self) - self._popup_is_shown = True - - def hidePopup(self): - """Reimplemented.""" - self.view().removeEventFilter(self) - self.view().viewport().removeEventFilter(self) - self._popup_is_shown = False - self._initial_mouse_pos = None - super(MultiSelectionComboBox, self).hidePopup() - self.view().clearFocus() - - def _event_popup_shown(self, obj, event): - if not self._popup_is_shown: - return - - current_index = self.view().currentIndex() - model = self.model() - - if event.type() == QtCore.QEvent.MouseMove: - if ( - self.view().isVisible() - and self._initial_mouse_pos is not None - and self._block_mouse_release_timer.isActive() - ): - diff = obj.mapToGlobal(event.pos()) - self._initial_mouse_pos - if diff.manhattanLength() > 9: - self._block_mouse_release_timer.stop() - return - - index_flags = current_index.flags() - state = checkstate_int_to_enum( - current_index.data(QtCore.Qt.CheckStateRole) - ) - new_state = None - - if event.type() == QtCore.QEvent.MouseButtonRelease: - if ( - self._block_mouse_release_timer.isActive() - or not current_index.isValid() - or not self.view().isVisible() - or not self.view().rect().contains(event.pos()) - or not index_flags & QtCore.Qt.ItemIsSelectable - or not index_flags & QtCore.Qt.ItemIsEnabled - or not index_flags & QtCore.Qt.ItemIsUserCheckable - ): - return - - if state == QtCore.Qt.Unchecked: - new_state = CHECKED_INT - else: - new_state = UNCHECKED_INT - - elif event.type() == QtCore.QEvent.KeyPress: - # TODO: handle QtCore.Qt.Key_Enter, Key_Return? - if event.key() == QtCore.Qt.Key_Space: - if ( - index_flags & QtCore.Qt.ItemIsUserCheckable - and index_flags & ITEM_IS_USER_TRISTATE - ): - new_state = (checkstate_enum_to_int(state) + 1) % 3 - - elif index_flags & QtCore.Qt.ItemIsUserCheckable: - # toggle the current items check state - if state != QtCore.Qt.Checked: - new_state = CHECKED_INT - else: - new_state = UNCHECKED_INT - - if new_state is not None: - model.setData(current_index, new_state, QtCore.Qt.CheckStateRole) - self.view().update(current_index) - self.update_size_hint() - self.value_changed.emit() - return True - - def eventFilter(self, obj, event): - """Reimplemented.""" - result = self._event_popup_shown(obj, event) - if result is not None: - return result - - return super(MultiSelectionComboBox, self).eventFilter(obj, event) - - def addItem(self, *args, **kwargs): - idx = self.count() - super(MultiSelectionComboBox, self).addItem(*args, **kwargs) - self.model().item(idx).setCheckable(True) - - def paintEvent(self, event): - """Reimplemented.""" - painter = QtWidgets.QStylePainter(self) - option = QtWidgets.QStyleOptionComboBox() - self.initStyleOption(option) - painter.drawComplexControl(QtWidgets.QStyle.CC_ComboBox, option) - - # draw the icon and text - items = self.checked_items_text() - if not items: - option.currentText = self.placeholder_text - option.palette.setCurrentColorGroup(QtGui.QPalette.Disabled) - painter.drawControl(QtWidgets.QStyle.CE_ComboBoxLabel, option) - return - - font_metricts = self.fontMetrics() - - if self.item_height is None: - self.updateGeometry() - self.update() - return - - for line, items in self.lines.items(): - top_y = ( - option.rect.top() - + (line * self.item_height) - + self.top_bottom_margins - ) - left_x = option.rect.left() + self.left_offset - for item in items: - label_rect = font_metricts.boundingRect(item) - label_height = label_rect.height() - - label_rect.moveTop(top_y) - label_rect.moveLeft(left_x) - label_rect.setHeight(self.item_height) - label_rect.setWidth( - label_rect.width() + self.left_right_padding - ) - - bg_rect = QtCore.QRectF(label_rect) - bg_rect.setWidth( - label_rect.width() + self.left_right_padding - ) - left_x = bg_rect.right() + self.item_spacing - - label_rect.moveLeft(label_rect.x() + self.left_right_padding) - - bg_rect.setHeight(label_height + (2 * self.top_bottom_padding)) - bg_rect.moveTop(bg_rect.top() + self.top_bottom_margins) - - path = QtGui.QPainterPath() - path.addRoundedRect(bg_rect, 5, 5) - - painter.fillPath(path, self.item_bg_color) - - painter.drawText( - label_rect, - QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, - item - ) - - def resizeEvent(self, *args, **kwargs): - super(MultiSelectionComboBox, self).resizeEvent(*args, **kwargs) - self.update_size_hint() - - def update_size_hint(self): - self.lines = {} - - items = self.checked_items_text() - if not items: - self.update() - return - - option = QtWidgets.QStyleOptionComboBox() - self.initStyleOption(option) - btn_rect = self.style().subControlRect( - QtWidgets.QStyle.CC_ComboBox, - option, - QtWidgets.QStyle.SC_ComboBoxArrow - ) - total_width = option.rect.width() - btn_rect.width() - - line = 0 - self.lines = {line: []} - - font_metricts = self.fontMetrics() - default_left_x = 0 + self.left_offset - left_x = int(default_left_x) - for item in items: - rect = font_metricts.boundingRect(item) - width = rect.width() + (2 * self.left_right_padding) - right_x = left_x + width - if right_x > total_width: - left_x = int(default_left_x) - if self.lines.get(line): - line += 1 - self.lines[line] = [item] - left_x += width - else: - self.lines[line] = [item] - line += 1 - else: - if line in self.lines: - self.lines[line].append(item) - else: - self.lines[line] = [item] - left_x = left_x + width + self.item_spacing - - self.update() - self.updateGeometry() - - def sizeHint(self): - value = super(MultiSelectionComboBox, self).sizeHint() - lines = len(self.lines) - if lines == 0: - lines = 1 - - if self.item_height is None: - self.item_height = ( - self.fontMetrics().height() - + (2 * self.top_bottom_padding) - + (2 * self.top_bottom_margins) - ) - value.setHeight( - (lines * self.item_height) - + (2 * self.top_bottom_margins) - ) - return value - - def setItemCheckState(self, index, state): - self.setItemData(index, state, QtCore.Qt.CheckStateRole) - - def set_value(self, values): - for idx in range(self.count()): - value = self.itemData(idx, role=QtCore.Qt.UserRole) - if value in values: - check_state = CHECKED_INT - else: - check_state = UNCHECKED_INT - self.setItemData(idx, check_state, QtCore.Qt.CheckStateRole) - self.update_size_hint() - - def value(self): - items = list() - for idx in range(self.count()): - state = checkstate_int_to_enum( - self.itemData(idx, role=QtCore.Qt.CheckStateRole) - ) - if state == QtCore.Qt.Checked: - items.append( - self.itemData(idx, role=QtCore.Qt.UserRole) - ) - return items - - def checked_items_text(self): - items = list() - for idx in range(self.count()): - state = checkstate_int_to_enum( - self.itemData(idx, role=QtCore.Qt.CheckStateRole) - ) - if state == QtCore.Qt.Checked: - items.append(self.itemText(idx)) - return items - - def wheelEvent(self, event): - event.ignore() - - def keyPressEvent(self, event): - if ( - event.key() == QtCore.Qt.Key_Down - and event.modifiers() & QtCore.Qt.AltModifier - ): - return self.showPopup() - - if event.key() in self.ignored_keys: - return event.ignore() - - return super(MultiSelectionComboBox, self).keyPressEvent(event) From cbc622c46f52fb784c6dc8c49d37c3b3d65f0c85 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 31 Aug 2023 11:23:38 +0200 Subject: [PATCH 231/327] update docstrings --- openpype/lib/attribute_definitions.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/openpype/lib/attribute_definitions.py b/openpype/lib/attribute_definitions.py index 44aa8dee48..a71709cace 100644 --- a/openpype/lib/attribute_definitions.py +++ b/openpype/lib/attribute_definitions.py @@ -427,9 +427,12 @@ class EnumDef(AbstractAttrDef): """Enumeration of single item from items. Args: - items: Items definition that can be converted using - 'prepare_enum_items'. - default: Default value. Must be one key(value) from passed items. + items (Union[list[str], list[dict[str, Any]]): Items definition that + can be converted using 'prepare_enum_items'. + default (Optional[Any]): Default value. Must be one key(value) from + passed items or list of values for multiselection. + multiselection (Optional[bool]): If True, multiselection is allowed. + Output is list of selected items. """ type = "enum" From 84e89aa422728daaac3f2550358cdc18d97a89f7 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 31 Aug 2023 11:39:07 +0200 Subject: [PATCH 232/327] Webpublisher: better encode list values for click (#5546) * Fix - list of arguments must be provided differently Targets could be a list, original implementation pushed it as a comma separated, it must be separated by space for click to understand. * Fix - split by space targets might come as a tuple from command line * Fix - better providing of arguments * Revert "Fix - split by space" This reverts commit 23884ac1c544e2d8003d82423a0da8b83821d426. --- .../webserver_service/webpublish_routes.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py b/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py index e56f245d27..20d585e906 100644 --- a/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py +++ b/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py @@ -280,13 +280,14 @@ class BatchPublishEndpoint(WebpublishApiEndpoint): for key, value in add_args.items(): # Skip key values where value is None - if value is not None: - args.append("--{}".format(key)) - # Extend list into arguments (targets can be a list) - if isinstance(value, (tuple, list)): - args.extend(value) - else: - args.append(value) + if value is None: + continue + arg_key = "--{}".format(key) + if not isinstance(value, (tuple, list)): + value = [value] + + for item in value: + args += [arg_key, item] log.info("args:: {}".format(args)) if add_to_queue: From d3c1b84835aed4579ef7909e52631ab0732d6796 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 31 Aug 2023 12:07:23 +0200 Subject: [PATCH 233/327] fixing paths slashes in nuke api --- openpype/hosts/nuke/api/lib.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 2a6c1fb12c..d6a5e67ba0 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2107,8 +2107,9 @@ class WorkfileSettings(object): # set ocio config path if config_data: + config_path = config_data["path"].replace("\\", "/") log.info("OCIO config path found: `{}`".format( - config_data["path"])) + config_path)) # check if there's a mismatch between environment and settings correct_settings = self._is_settings_matching_environment( @@ -2233,7 +2234,7 @@ Reopening Nuke should synchronize these paths and resolve any discrepancies. Returns: str: OCIO config path with environment variable TCL expression """ - config_path = config_data["path"] + config_path = config_data["path"].replace("\\", "/") config_template = config_data["template"] included_vars = self._get_included_vars(config_template) From e9c3a84916a87ef8df717b4f75b698f55b7cd5dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Thu, 31 Aug 2023 12:11:22 +0200 Subject: [PATCH 234/327] Update openpype/hosts/nuke/api/lib.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/nuke/api/lib.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index cedbe6d5e6..157300d150 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -3371,10 +3371,9 @@ def create_viewer_profile_string(viewer, display=None, path_like=False): Returns: str: viewer config string """ - if display: - if path_like: - return "{}/{}".format(display, viewer) - else: - return "{} ({})".format(viewer, display) - else: + if not display: return viewer + + if path_like: + return "{}/{}".format(display, viewer) + return "{} ({})".format(viewer, display) From 33e18661241a59a612a06558fbc19f5de264220b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 31 Aug 2023 13:30:33 +0200 Subject: [PATCH 235/327] separating code into own function for monitor lut settings --- openpype/hosts/nuke/api/lib.py | 64 ++++++++++++++++++++++------------ 1 file changed, 42 insertions(+), 22 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 157300d150..95e945057c 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2092,30 +2092,14 @@ class WorkfileSettings(object): workfile_settings.pop("colorManagement", None) workfile_settings.pop("OCIO_config", None) - # treat monitor lut separately + # get monitor lut from settings respecting Nuke version differences monitor_lut = workfile_settings.pop("monitorLut", None) - m_display, m_viewer = get_viewer_config_from_string(monitor_lut) - v_display, v_viewer = get_viewer_config_from_string( - viewer_process_settings - ) + monitor_lut_data = self._get_monitor_settings( + viewer_process_settings, monitor_lut) - # set monitor lut differently for nuke version 14 - if nuke.NUKE_VERSION_MAJOR >= 14: - workfile_settings["monitorOutLUT"] = create_viewer_profile_string( - m_viewer, m_display, path_like=False) - # monitorLut=thumbnails - viewerProcess makes more sense - workfile_settings["monitorLut"] = create_viewer_profile_string( - v_viewer, v_display, path_like=False) - - if nuke.NUKE_VERSION_MAJOR == 13: - workfile_settings["monitorOutLUT"] = create_viewer_profile_string( - m_viewer, m_display, path_like=False) - # monitorLut=thumbnails - viewerProcess makes more sense - workfile_settings["monitorLut"] = create_viewer_profile_string( - v_viewer, v_display, path_like=True) - if nuke.NUKE_VERSION_MAJOR <= 12: - workfile_settings["monitorLut"] = create_viewer_profile_string( - m_viewer, m_display, path_like=True) + # set monitor related knobs luts (MonitorOut, Thumbnails) + for knob, value_ in monitor_lut_data.items(): + workfile_settings[knob] = value_ # then set the rest for knob, value_ in workfile_settings.items(): @@ -2144,6 +2128,42 @@ class WorkfileSettings(object): if correct_settings: self._set_ocio_config_path_to_workfile(config_data) + def _get_monitor_settings(self, viewer_lut, monitor_lut): + """ Get monitor settings from viewer and monitor lut + + Args: + viewer_lut (str): viewer lut string + monitor_lut (str): monitor lut string + + Returns: + dict: monitor settings + """ + output_data = {} + m_display, m_viewer = get_viewer_config_from_string(monitor_lut) + v_display, v_viewer = get_viewer_config_from_string( + viewer_lut + ) + + # set monitor lut differently for nuke version 14 + if nuke.NUKE_VERSION_MAJOR >= 14: + output_data["monitorOutLUT"] = create_viewer_profile_string( + m_viewer, m_display, path_like=False) + # monitorLut=thumbnails - viewerProcess makes more sense + output_data["monitorLut"] = create_viewer_profile_string( + v_viewer, v_display, path_like=False) + + if nuke.NUKE_VERSION_MAJOR == 13: + output_data["monitorOutLUT"] = create_viewer_profile_string( + m_viewer, m_display, path_like=False) + # monitorLut=thumbnails - viewerProcess makes more sense + output_data["monitorLut"] = create_viewer_profile_string( + v_viewer, v_display, path_like=True) + if nuke.NUKE_VERSION_MAJOR <= 12: + output_data["monitorLut"] = create_viewer_profile_string( + m_viewer, m_display, path_like=True) + + return output_data + def _is_settings_matching_environment(self, config_data): """ Check if OCIO config path is different from environment From 82b50dcfc2698e60b1ffb45723a4112a0865f8e4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 31 Aug 2023 14:00:13 +0200 Subject: [PATCH 236/327] adding comments --- .../plugins/publish/validate_rendered_frames.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py b/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py index ef3d4d0bb5..e316c6ff6e 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py +++ b/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py @@ -17,18 +17,23 @@ class RepairActionBase(pyblish.api.Action): def repair_knob(self, context, instances, state): create_context = context.data["create_context"] for instance in instances: - files_remove = [ + files_to_remove = [ + # create full path to file os.path.join(instance.data["outputDir"], f_) + # iterate representations from instance data for r_ in instance.data.get("representations", []) + # make sure that the representation has files in list + if r_.get("files") and isinstance(r_.get("files"), list) + # iterate files from representation files list for f_ in r_.get("files", []) ] - self.log.info("Files to be removed: {}".format(files_remove)) - for f_ in files_remove: + self.log.info("Files to be removed: {}".format(files_to_remove)) + for f_ in files_to_remove: os.remove(f_) self.log.debug("removing file: {}".format(f_)) # Reset the render knob - instance_id = instance.data["instance_id"] + instance_id = instance.data.get("instance_id") created_instance = create_context.get_instance_by_id( instance_id ) From 1a3c8ad77252ffa9217c6af98ba27dcdd56c366a Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 31 Aug 2023 17:34:42 +0300 Subject: [PATCH 237/327] make validateReviewColorspace optional --- .../plugins/publish/validate_review_colorspace.py | 10 ++++++++-- .../defaults/project_settings/houdini.json | 5 +++++ .../schemas/schema_houdini_publish.json | 6 +++++- .../houdini/server/settings/publish_plugins.py | 14 +++++++++++--- server_addon/houdini/server/version.py | 2 +- 5 files changed, 30 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py index 2c7420bf48..f680f62142 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py +++ b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py @@ -1,6 +1,9 @@ # -*- coding: utf-8 -*- import pyblish.api -from openpype.pipeline import PublishValidationError +from openpype.pipeline import ( + PublishValidationError, + OptionalPyblishPluginMixin +) from openpype.pipeline.publish import RepairAction from openpype.hosts.houdini.api.action import SelectROPAction from openpype.hosts.houdini.api.colorspace import get_default_display_view_colorspace # noqa @@ -14,7 +17,8 @@ class SetDefaultViewSpaceAction(RepairAction): icon = "mdi.monitor" -class ValidateReviewColorspace(pyblish.api.InstancePlugin): +class ValidateReviewColorspace(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Validate Review Colorspace parameters. It checks if 'OCIO Colorspace' parameter was set to valid value. @@ -26,6 +30,8 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin): label = "Validate Review Colorspace" actions = [SetDefaultViewSpaceAction, SelectROPAction] + optional = True + def process(self, instance): invalid = self.get_invalid(instance) if invalid: diff --git a/openpype/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json index 9d047c28bd..93d5c50d5e 100644 --- a/openpype/settings/defaults/project_settings/houdini.json +++ b/openpype/settings/defaults/project_settings/houdini.json @@ -93,6 +93,11 @@ "$JOB" ] }, + "ValidateReviewColorspace": { + "enabled": true, + "optional": true, + "active": true + }, "ValidateContainers": { "enabled": true, "optional": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_publish.json index aa6eaf5164..b57089007e 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_publish.json @@ -40,6 +40,10 @@ "type": "schema_template", "name": "template_publish_plugin", "template_data": [ + { + "key": "ValidateReviewColorspace", + "label": "Validate Review Colorspace" + }, { "key": "ValidateContainers", "label": "ValidateContainers" @@ -47,4 +51,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/server_addon/houdini/server/settings/publish_plugins.py b/server_addon/houdini/server/settings/publish_plugins.py index 7d35d7e634..4534d8d0d9 100644 --- a/server_addon/houdini/server/settings/publish_plugins.py +++ b/server_addon/houdini/server/settings/publish_plugins.py @@ -120,7 +120,7 @@ class ValidateWorkfilePathsModel(BaseSettingsModel): ) -class ValidateContainersModel(BaseSettingsModel): +class BasicValidateModel(BaseSettingsModel): enabled: bool = Field(title="Enabled") optional: bool = Field(title="Optional") active: bool = Field(title="Active") @@ -130,8 +130,11 @@ class PublishPluginsModel(BaseSettingsModel): ValidateWorkfilePaths: ValidateWorkfilePathsModel = Field( default_factory=ValidateWorkfilePathsModel, title="Validate workfile paths settings.") - ValidateContainers: ValidateContainersModel = Field( - default_factory=ValidateContainersModel, + ValidateReviewColorspace: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Review Colorspace.") + ValidateContainers: BasicValidateModel = Field( + default_factory=BasicValidateModel, title="Validate Latest Containers.") @@ -148,6 +151,11 @@ DEFAULT_HOUDINI_PUBLISH_SETTINGS = { "$JOB" ] }, + "ValidateReviewColorspace": { + "enabled": True, + "optional": True, + "active": True + }, "ValidateContainers": { "enabled": True, "optional": True, diff --git a/server_addon/houdini/server/version.py b/server_addon/houdini/server/version.py index 485f44ac21..b3f4756216 100644 --- a/server_addon/houdini/server/version.py +++ b/server_addon/houdini/server/version.py @@ -1 +1 @@ -__version__ = "0.1.1" +__version__ = "0.1.2" From 4d94bec464349db6e5277ae9d9615f15bf73e991 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 31 Aug 2023 22:55:59 +0800 Subject: [PATCH 238/327] oscar's comment on cameras implemenetation --- openpype/hosts/max/plugins/publish/collect_render.py | 4 +--- .../modules/deadline/plugins/publish/submit_max_deadline.py | 5 ++--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index e7730369b6..8ee2f43103 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -35,9 +35,7 @@ class CollectRender(pyblish.api.InstancePlugin): files_by_aov.update(aovs) camera = rt.viewport.GetCamera() - instance.data["cameras"] = None - if camera: - instance.data["cameras"] = [camera.name] + instance.data["cameras"] = [camera.name] if camera else None # noqa if "expectedFiles" not in instance.data: instance.data["expectedFiles"] = list() diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index 6b42270d05..01132662d4 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -31,7 +31,6 @@ class MaxPluginInfo(object): Version = attr.ib(default=None) # Mandatory for Deadline SaveFile = attr.ib(default=True) IgnoreInputs = attr.ib(default=True) - Camera = attr.ib(default=None) class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, @@ -155,8 +154,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, SceneFile=self.scene_path, Version=instance.data["maxversion"], SaveFile=True, - IgnoreInputs=True, - Camera=instance.data["cameras"][0] + IgnoreInputs=True ) plugin_payload = attr.asdict(plugin_info) @@ -242,6 +240,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, "separateAovFiles") if instance.data["cameras"]: plugin_info["Camera0"] = None + plugin_info["Camera"] = instance.data["cameras"][0] plugin_info["Camera1"] = instance.data["cameras"][0] self.log.debug("plugin data:{}".format(plugin_data)) plugin_info.update(plugin_data) From e2e3bb3a68309c13e9f53fb5713baea6df6704dd Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 31 Aug 2023 17:23:38 +0200 Subject: [PATCH 239/327] AYON: Fill entities during editorial (2) (#5549) * fix appending of instances by asset name * skip disabled instances * formatting fix --- openpype/plugins/publish/extract_hierarchy_to_ayon.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_hierarchy_to_ayon.py b/openpype/plugins/publish/extract_hierarchy_to_ayon.py index de9a70c233..36a7042ba5 100644 --- a/openpype/plugins/publish/extract_hierarchy_to_ayon.py +++ b/openpype/plugins/publish/extract_hierarchy_to_ayon.py @@ -42,13 +42,16 @@ class ExtractHierarchyToAYON(pyblish.api.ContextPlugin): def _fill_instance_entities(self, context, project_name): instances_by_asset_name = collections.defaultdict(list) for instance in context: + if instance.data.get("publish") is False: + continue + instance_entity = instance.data.get("assetEntity") if instance_entity: continue # Skip if instance asset does not match instance_asset_name = instance.data.get("asset") - instances_by_asset_name[instance_asset_name] = instance + instances_by_asset_name[instance_asset_name].append(instance) project_doc = context.data["projectEntity"] asset_docs = get_assets( From cf8919dbc5336e29e121020a7506d70277386834 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 31 Aug 2023 17:24:52 +0200 Subject: [PATCH 240/327] AYON: Update settings (#5544) * added 3dsmax settings conversion * added conversion of reposition knobs * fixed matching defaults * updated core settings * updated aftereffects settings model --- openpype/settings/ayon_settings.py | 29 ++++++++++ .../server/settings/creator_plugins.py | 2 +- .../aftereffects/server/settings/main.py | 2 +- server_addon/aftereffects/server/version.py | 2 +- server_addon/core/server/settings/main.py | 53 +++++++++++++++++-- server_addon/core/server/version.py | 2 +- .../max/server/settings/render_settings.py | 2 +- server_addon/maya/server/settings/creators.py | 4 +- .../server/settings/simple_creators.py | 17 ++++++ 9 files changed, 104 insertions(+), 9 deletions(-) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index 50abfe4839..9a4f0607e0 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -616,6 +616,23 @@ def _convert_maya_project_settings(ayon_settings, output): output["maya"] = ayon_maya +def _convert_3dsmax_project_settings(ayon_settings, output): + if "max" not in ayon_settings: + return + + ayon_max = ayon_settings["max"] + _convert_host_imageio(ayon_max) + if "PointCloud" in ayon_max: + point_cloud_attribute = ayon_max["PointCloud"]["attribute"] + new_point_cloud_attribute = { + item["name"]: item["value"] + for item in point_cloud_attribute + } + ayon_max["PointCloud"]["attribute"] = new_point_cloud_attribute + + output["max"] = ayon_max + + def _convert_nuke_knobs(knobs): new_knobs = [] for knob in knobs: @@ -737,6 +754,17 @@ def _convert_nuke_project_settings(ayon_settings, output): item_filter["subsets"] = item_filter.pop("product_names") item_filter["families"] = item_filter.pop("product_types") + reformat_nodes_config = item.get("reformat_nodes_config") or {} + reposition_nodes = reformat_nodes_config.get( + "reposition_nodes") or [] + + for reposition_node in reposition_nodes: + if "knobs" not in reposition_node: + continue + reposition_node["knobs"] = _convert_nuke_knobs( + reposition_node["knobs"] + ) + name = item.pop("name") new_review_data_outputs[name] = item ayon_publish["ExtractReviewDataMov"]["outputs"] = new_review_data_outputs @@ -1261,6 +1289,7 @@ def convert_project_settings(ayon_settings, default_settings): _convert_flame_project_settings(ayon_settings, output) _convert_fusion_project_settings(ayon_settings, output) _convert_maya_project_settings(ayon_settings, output) + _convert_3dsmax_project_settings(ayon_settings, output) _convert_nuke_project_settings(ayon_settings, output) _convert_hiero_project_settings(ayon_settings, output) _convert_photoshop_project_settings(ayon_settings, output) diff --git a/server_addon/aftereffects/server/settings/creator_plugins.py b/server_addon/aftereffects/server/settings/creator_plugins.py index ee52fadd40..9cb03b0b26 100644 --- a/server_addon/aftereffects/server/settings/creator_plugins.py +++ b/server_addon/aftereffects/server/settings/creator_plugins.py @@ -5,7 +5,7 @@ from ayon_server.settings import BaseSettingsModel class CreateRenderPlugin(BaseSettingsModel): mark_for_review: bool = Field(True, title="Review") - defaults: list[str] = Field( + default_variants: list[str] = Field( default_factory=list, title="Default Variants" ) diff --git a/server_addon/aftereffects/server/settings/main.py b/server_addon/aftereffects/server/settings/main.py index 04d2e51cc9..4edc46d259 100644 --- a/server_addon/aftereffects/server/settings/main.py +++ b/server_addon/aftereffects/server/settings/main.py @@ -40,7 +40,7 @@ DEFAULT_AFTEREFFECTS_SETTING = { "create": { "RenderCreator": { "mark_for_review": True, - "defaults": [ + "default_variants": [ "Main" ] } diff --git a/server_addon/aftereffects/server/version.py b/server_addon/aftereffects/server/version.py index a242f0e757..df0c92f1e2 100644 --- a/server_addon/aftereffects/server/version.py +++ b/server_addon/aftereffects/server/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring addon version.""" -__version__ = "0.1.1" +__version__ = "0.1.2" diff --git a/server_addon/core/server/settings/main.py b/server_addon/core/server/settings/main.py index d19d732e71..ca8f7e63ed 100644 --- a/server_addon/core/server/settings/main.py +++ b/server_addon/core/server/settings/main.py @@ -4,6 +4,7 @@ from ayon_server.settings import ( BaseSettingsModel, MultiplatformPathListModel, ensure_unique_names, + task_types_enum, ) from ayon_server.exceptions import BadRequestException @@ -38,13 +39,52 @@ class CoreImageIOConfigModel(BaseSettingsModel): class CoreImageIOBaseModel(BaseSettingsModel): activate_global_color_management: bool = Field( False, - title="Override global OCIO config" + title="Enable Color Management" ) ocio_config: CoreImageIOConfigModel = Field( - default_factory=CoreImageIOConfigModel, title="OCIO config" + default_factory=CoreImageIOConfigModel, + title="OCIO config" ) file_rules: CoreImageIOFileRulesModel = Field( - default_factory=CoreImageIOFileRulesModel, title="File Rules" + default_factory=CoreImageIOFileRulesModel, + title="File Rules" + ) + + +class VersionStartCategoryProfileModel(BaseSettingsModel): + _layout = "expanded" + host_names: list[str] = Field( + default_factory=list, + title="Host names" + ) + task_types: list[str] = Field( + default_factory=list, + title="Task types", + enum_resolver=task_types_enum + ) + task_names: list[str] = Field( + default_factory=list, + title="Task names" + ) + product_types: list[str] = Field( + default_factory=list, + title="Product types" + ) + product_names: list[str] = Field( + default_factory=list, + title="Product names" + ) + version_start: int = Field( + 1, + title="Version Start", + ge=0 + ) + + +class VersionStartCategoryModel(BaseSettingsModel): + profiles: list[VersionStartCategoryProfileModel] = Field( + default_factory=list, + title="Profiles" ) @@ -61,6 +101,10 @@ class CoreSettings(BaseSettingsModel): default_factory=GlobalToolsModel, title="Tools" ) + version_start_category: VersionStartCategoryModel = Field( + default_factory=VersionStartCategoryModel, + title="Version start" + ) imageio: CoreImageIOBaseModel = Field( default_factory=CoreImageIOBaseModel, title="Color Management (ImageIO)" @@ -131,6 +175,9 @@ DEFAULT_VALUES = { "studio_code": "", "environments": "{}", "tools": DEFAULT_TOOLS_VALUES, + "version_start_category": { + "profiles": [] + }, "publish": DEFAULT_PUBLISH_VALUES, "project_folder_structure": json.dumps({ "__project_root__": { diff --git a/server_addon/core/server/version.py b/server_addon/core/server/version.py index 485f44ac21..b3f4756216 100644 --- a/server_addon/core/server/version.py +++ b/server_addon/core/server/version.py @@ -1 +1 @@ -__version__ = "0.1.1" +__version__ = "0.1.2" diff --git a/server_addon/max/server/settings/render_settings.py b/server_addon/max/server/settings/render_settings.py index 6c236d9f12..c00cb5e436 100644 --- a/server_addon/max/server/settings/render_settings.py +++ b/server_addon/max/server/settings/render_settings.py @@ -44,6 +44,6 @@ class RenderSettingsModel(BaseSettingsModel): DEFAULT_RENDER_SETTINGS = { "default_render_image_folder": "renders/3dsmax", "aov_separator": "underscore", - "image_format": "png", + "image_format": "exr", "multipass": True } diff --git a/server_addon/maya/server/settings/creators.py b/server_addon/maya/server/settings/creators.py index 9b97b92e59..11e2b8a36c 100644 --- a/server_addon/maya/server/settings/creators.py +++ b/server_addon/maya/server/settings/creators.py @@ -252,7 +252,9 @@ DEFAULT_CREATORS_SETTINGS = { }, "CreateUnrealSkeletalMesh": { "enabled": True, - "default_variants": [], + "default_variants": [ + "Main", + ], "joint_hints": "jnt_org" }, "CreateMultiverseLook": { diff --git a/server_addon/traypublisher/server/settings/simple_creators.py b/server_addon/traypublisher/server/settings/simple_creators.py index 94d6602738..8335b9d34e 100644 --- a/server_addon/traypublisher/server/settings/simple_creators.py +++ b/server_addon/traypublisher/server/settings/simple_creators.py @@ -288,5 +288,22 @@ DEFAULT_SIMPLE_CREATORS = [ "allow_multiple_items": True, "allow_version_control": False, "extensions": [] + }, + { + "product_type": "audio", + "identifier": "", + "label": "Audio ", + "icon": "fa5s.file-audio", + "default_variants": [ + "Main" + ], + "description": "Audio product", + "detailed_description": "Audio files for review or final delivery", + "allow_sequences": False, + "allow_multiple_items": False, + "allow_version_control": False, + "extensions": [ + ".wav" + ] } ] From 13d1b407199e7012aa309a6072a405a2d35cf657 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Thu, 31 Aug 2023 17:38:47 +0200 Subject: [PATCH 241/327] Update openpype/hosts/nuke/api/lib.py Co-authored-by: Roy Nieterau --- openpype/hosts/nuke/api/lib.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 95e945057c..91a9294740 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2140,9 +2140,7 @@ class WorkfileSettings(object): """ output_data = {} m_display, m_viewer = get_viewer_config_from_string(monitor_lut) - v_display, v_viewer = get_viewer_config_from_string( - viewer_lut - ) + v_display, v_viewer = get_viewer_config_from_string(viewer_lut) # set monitor lut differently for nuke version 14 if nuke.NUKE_VERSION_MAJOR >= 14: From 1a936c155985e804f0ddc01e14a4573c56bbb8e7 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 31 Aug 2023 22:42:13 +0300 Subject: [PATCH 242/327] BigRoy's comments --- .../houdini/plugins/publish/validate_review_colorspace.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py index f680f62142..3f5e5bc354 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py +++ b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py @@ -13,7 +13,7 @@ import hou class SetDefaultViewSpaceAction(RepairAction): - label = "Set default view space" + label = "Set default view colorspace" icon = "mdi.monitor" @@ -33,6 +33,10 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin, optional = True def process(self, instance): + + if not self.is_active(instance.data): + return + invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( From 24cbaefe953589b39f681bf4b93cc992cbb9fa19 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 31 Aug 2023 22:29:18 +0200 Subject: [PATCH 243/327] removing redundant action it was residual approach from old workflow --- .../plugins/publish/validate_rendered_frames.py | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py b/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py index e316c6ff6e..9a35b61a0e 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py +++ b/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py @@ -17,29 +17,14 @@ class RepairActionBase(pyblish.api.Action): def repair_knob(self, context, instances, state): create_context = context.data["create_context"] for instance in instances: - files_to_remove = [ - # create full path to file - os.path.join(instance.data["outputDir"], f_) - # iterate representations from instance data - for r_ in instance.data.get("representations", []) - # make sure that the representation has files in list - if r_.get("files") and isinstance(r_.get("files"), list) - # iterate files from representation files list - for f_ in r_.get("files", []) - ] - self.log.info("Files to be removed: {}".format(files_to_remove)) - for f_ in files_to_remove: - os.remove(f_) - self.log.debug("removing file: {}".format(f_)) - # Reset the render knob instance_id = instance.data.get("instance_id") created_instance = create_context.get_instance_by_id( instance_id ) created_instance.creator_attributes["render_target"] = state - self.log.info("Rendering toggled to `{}`".format(state)) + create_context.save_changes() From ec8a0f49480c8359f6b501f73626d770c12fe931 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 31 Aug 2023 22:44:36 +0200 Subject: [PATCH 244/327] simplification of spaghetti code and improving logic --- .../plugins/publish/collect_farm_target.py | 40 +++++++------------ 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/openpype/plugins/publish/collect_farm_target.py b/openpype/plugins/publish/collect_farm_target.py index 78410835dd..adcd842b48 100644 --- a/openpype/plugins/publish/collect_farm_target.py +++ b/openpype/plugins/publish/collect_farm_target.py @@ -15,31 +15,21 @@ class CollectFarmTarget(pyblish.api.InstancePlugin): return context = instance.context - try: - deadline_module = context.data.get("openPypeModules")["deadline"] - if deadline_module.enabled: - instance.data["toBeRenderedOn"] = "deadline" - self.log.debug("Collected render target: deadline") - except AttributeError: - self.log.error("Cannot get OpenPype Deadline module.") - raise AssertionError("OpenPype Deadline module not found.") - try: - royalrender_module = \ - context.data.get("openPypeModules")["royalrender"] - if royalrender_module.enabled: - instance.data["toBeRenderedOn"] = "royalrender" - self.log.debug("Collected render target: royalrender") + farm_name = "" + op_modules = context.data.get("openPypeModules") - except AttributeError: - self.log.error("Cannot get OpenPype RoyalRender module.") - raise AssertionError("OpenPype RoyalRender module not found.") + for farm_renderer in ["deadline", "royalrender", "muster"]: + op_module = op_modules.get(farm_renderer, False) - try: - muster_module = context.data.get("openPypeModules")["muster"] - if muster_module.enabled: - instance.data["toBeRenderedOn"] = "muster" - self.log.debug("Collected render target: muster") - except AttributeError: - self.log.error("Cannot get OpenPype Muster module.") - raise AssertionError("OpenPype Muster module not found.") + if op_module and op_module.enabled: + farm_name = farm_renderer + elif not op_module: + self.log.error("Cannot get OpenPype {0} module.".format( + farm_renderer)) + + if farm_name: + self.log.debug("Collected render target: {0}".format(farm_name)) + instance.data["toBeRenderedOn"] = farm_name + else: + AssertionError("No OpenPype renderer module found") From d4393bad6b466438a839786903152fc36e293cae Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 31 Aug 2023 23:18:52 +0200 Subject: [PATCH 245/327] changing signature since we only need context.data for input. --- openpype/pipeline/colorspace.py | 39 ++++++-------------- openpype/pipeline/publish/publish_plugins.py | 8 +--- 2 files changed, 13 insertions(+), 34 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 649d355f62..b7728936b0 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -617,7 +617,7 @@ def get_colorspace_settings_from_publish_context(context_data): Returns: tuple | bool: config, file rules or None """ - if "imageioSettings" in context_data: + if "imageioSettings" in context_data and context_data["imageioSettings"]: return context_data["imageioSettings"] project_name = context_data["projectName"] @@ -631,14 +631,13 @@ def get_colorspace_settings_from_publish_context(context_data): anatomy_data=anatomy_data ) - # in case host color management is not enabled - if not config_data: - return None - - file_rules = get_imageio_file_rules( - project_name, host_name, - project_settings=project_settings_ - ) + # caching invalid state, so it's not recalculated all the time + file_rules = None + if config_data: + file_rules = get_imageio_file_rules( + project_name, host_name, + project_settings=project_settings_ + ) # caching settings for future instance processing context_data["imageioSettings"] = (config_data, file_rules) @@ -649,7 +648,6 @@ def get_colorspace_settings_from_publish_context(context_data): def set_colorspace_data_to_representation( representation, context_data, colorspace=None, - colorspace_settings=None, log=None ): """Sets colorspace data to representation. @@ -657,12 +655,7 @@ def set_colorspace_data_to_representation( Args: representation (dict): publishing representation context_data (publish.Context.data): publishing context data - config_data (dict): host resolved config data - file_rules (dict): host resolved file rules data colorspace (str, optional): colorspace name. Defaults to None. - colorspace_settings (tuple[dict, dict], optional): - Settings for config_data and file_rules. - Defaults to None. log (logging.Logger, optional): logger instance. Defaults to None. Example: @@ -691,21 +684,13 @@ def set_colorspace_data_to_representation( ) return - if colorspace_settings is None: - colorspace_settings = get_colorspace_settings_from_publish_context( - context_data) + # get colorspace settings + config_data, file_rules = get_colorspace_settings_from_publish_context( + context_data) # in case host color management is not enabled - if not colorspace_settings: - log.warning("Host's colorspace management is disabled.") - return - - # unpack colorspace settings - config_data, file_rules = colorspace_settings - if not config_data: - # warn in case no colorspace path was defined - log.warning("No colorspace management was defined") + log.warning("Host's colorspace management is disabled.") return log.debug("Config data is: `{}`".format(config_data)) diff --git a/openpype/pipeline/publish/publish_plugins.py b/openpype/pipeline/publish/publish_plugins.py index 17ede069cb..ae6cbc42d1 100644 --- a/openpype/pipeline/publish/publish_plugins.py +++ b/openpype/pipeline/publish/publish_plugins.py @@ -319,19 +319,13 @@ class ColormanagedPyblishPluginMixin(object): def set_representation_colorspace( self, representation, context, colorspace=None, - colorspace_settings=None ): """Sets colorspace data to representation. Args: representation (dict): publishing representation context (publish.Context): publishing context - config_data (dict): host resolved config data - file_rules (dict): host resolved file rules data colorspace (str, optional): colorspace name. Defaults to None. - colorspace_settings (tuple[dict, dict], optional): - Settings for config_data and file_rules. - Defaults to None. Example: ``` @@ -348,10 +342,10 @@ class ColormanagedPyblishPluginMixin(object): ``` """ + # using cached settings if available set_colorspace_data_to_representation( representation, context.data, colorspace, - colorspace_settings, log=self.log ) From 1f057525dd6dd2f3d03fbb97f68ee8504ce9aae5 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 31 Aug 2023 23:28:12 +0200 Subject: [PATCH 246/327] testing: fixing zip file ID --- tests/unit/openpype/pipeline/test_colorspace.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/unit/openpype/pipeline/test_colorspace.py b/tests/unit/openpype/pipeline/test_colorspace.py index c22acee2d4..ac35a28303 100644 --- a/tests/unit/openpype/pipeline/test_colorspace.py +++ b/tests/unit/openpype/pipeline/test_colorspace.py @@ -28,10 +28,9 @@ class TestPipelineColorspace(TestPipeline): cd to OpenPype repo root dir poetry run python ./start.py runtests ../tests/unit/openpype/pipeline """ - TEST_FILES = [ ( - "1Lf-mFxev7xiwZCWfImlRcw7Fj8XgNQMh", + "1csqimz8bbNcNgxtEXklLz6GRv91D3KgA", "test_pipeline_colorspace.zip", "" ) From 78b5ae149a89223cb2f5c8a59464510685a8d67a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Sep 2023 15:54:42 +0200 Subject: [PATCH 247/327] only project settings are passed by default to create plugin --- openpype/pipeline/create/context.py | 19 ++++++++++------- openpype/pipeline/create/creator_plugins.py | 23 ++++++++++++++------- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 3076efcde7..e0e2c09f96 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -16,6 +16,7 @@ from openpype.settings import ( get_system_settings, get_project_settings ) +from openpype.lib import is_func_signature_supported from openpype.lib.attribute_definitions import ( UnknownDef, serialize_attr_defs, @@ -1774,7 +1775,7 @@ class CreateContext: self.creator_discover_result = report for creator_class in report.plugins: if inspect.isabstract(creator_class): - self.log.info( + self.log.debug( "Skipping abstract Creator {}".format(str(creator_class)) ) continue @@ -1798,12 +1799,16 @@ class CreateContext: ).format(creator_class.host_name, self.host_name)) continue - creator = creator_class( - project_settings, - system_settings, - self, - self.headless - ) + if is_func_signature_supported( + creator_class, project_settings, self, self.headless + ): + creator = creator_class(project_settings, self, self.headless) + else: + # Backwards compatibility to pass system settings to creators + creator = creator_class( + project_settings, system_settings, self, self.headless + ) + if not creator.enabled: disabled_creators[creator_identifier] = creator continue diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index 38d6b6f465..a1a6a73236 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -10,7 +10,7 @@ from abc import ( import six from openpype.settings import get_system_settings, get_project_settings -from openpype.lib import Logger +from openpype.lib import Logger, is_func_signature_supported from openpype.pipeline.plugin_discover import ( discover, register_plugin, @@ -161,7 +161,6 @@ class BaseCreator: Args: project_settings (Dict[str, Any]): Project settings. - system_settings (Dict[str, Any]): System settings. create_context (CreateContext): Context which initialized creator. headless (bool): Running in headless mode. """ @@ -197,9 +196,7 @@ class BaseCreator: # QUESTION make this required? host_name = None - def __init__( - self, project_settings, system_settings, create_context, headless=False - ): + def __init__(self, project_settings, create_context, headless=False): # Reference to CreateContext self.create_context = create_context self.project_settings = project_settings @@ -208,10 +205,20 @@ class BaseCreator: # - we may use UI inside processing this attribute should be checked self.headless = headless - self.apply_settings(project_settings, system_settings) + if is_func_signature_supported( + self.apply_settings, project_settings + ): + self.apply_settings(project_settings) + else: + # Backwards compatibility for system settings + self.apply_settings(project_settings, {}) - def apply_settings(self, project_settings, system_settings): - """Method called on initialization of plugin to apply settings.""" + def apply_settings(self, project_settings): + """Method called on initialization of plugin to apply settings. + + Args: + project_settings (dict[str, Any]): Project settings. + """ pass From 1f6df907eb4934d471dcbad96b10d1c08566d5ca Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Sep 2023 16:54:05 +0200 Subject: [PATCH 248/327] fixed typehints --- openpype/lib/python_module_tools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/lib/python_module_tools.py b/openpype/lib/python_module_tools.py index a10263f991..bedf19562d 100644 --- a/openpype/lib/python_module_tools.py +++ b/openpype/lib/python_module_tools.py @@ -270,8 +270,8 @@ def is_func_signature_supported(func, *args, **kwargs): Args: func (function): A function where the signature should be tested. - *args (tuple[Any]): Positional arguments for function signature. - **kwargs (dict[str, Any]): Keyword arguments for function signature. + *args (Any): Positional arguments for function signature. + **kwargs (Any): Keyword arguments for function signature. Returns: bool: Function can pass in arguments. From fee3c950d3a728ef9d22bfc857eb14a15620ce5b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Sep 2023 17:00:14 +0200 Subject: [PATCH 249/327] removed deprecated 'abstractproperty' --- openpype/pipeline/create/creator_plugins.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index a1a6a73236..b38c1199e6 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -1,11 +1,7 @@ import copy import collections -from abc import ( - ABCMeta, - abstractmethod, - abstractproperty -) +from abc import ABCMeta, abstractmethod import six @@ -84,7 +80,8 @@ class SubsetConvertorPlugin(object): def host(self): return self._create_context.host - @abstractproperty + @property + @abstractmethod def identifier(self): """Converted identifier. @@ -231,7 +228,8 @@ class BaseCreator: return self.family - @abstractproperty + @property + @abstractmethod def family(self): """Family that plugin represents.""" From e2c60831c2a32e260615f9842e7ecf50109b48a1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Sep 2023 17:07:28 +0200 Subject: [PATCH 250/327] apply settings in TVPaint does not expect system settings --- openpype/hosts/tvpaint/plugins/create/create_render.py | 2 +- openpype/hosts/tvpaint/plugins/create/create_review.py | 2 +- openpype/hosts/tvpaint/plugins/create/create_workfile.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render.py b/openpype/hosts/tvpaint/plugins/create/create_render.py index 2369c7329f..656ea5d80b 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render.py @@ -139,7 +139,7 @@ class CreateRenderlayer(TVPaintCreator): # - Mark by default instance for review mark_for_review = True - def apply_settings(self, project_settings, system_settings): + def apply_settings(self, project_settings): plugin_settings = ( project_settings["tvpaint"]["create"]["create_render_layer"] ) diff --git a/openpype/hosts/tvpaint/plugins/create/create_review.py b/openpype/hosts/tvpaint/plugins/create/create_review.py index 886dae7c39..7bb7510a8e 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_review.py +++ b/openpype/hosts/tvpaint/plugins/create/create_review.py @@ -12,7 +12,7 @@ class TVPaintReviewCreator(TVPaintAutoCreator): # Settings active_on_create = True - def apply_settings(self, project_settings, system_settings): + def apply_settings(self, project_settings): plugin_settings = ( project_settings["tvpaint"]["create"]["create_review"] ) diff --git a/openpype/hosts/tvpaint/plugins/create/create_workfile.py b/openpype/hosts/tvpaint/plugins/create/create_workfile.py index 41347576d5..c3982c0eca 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_workfile.py +++ b/openpype/hosts/tvpaint/plugins/create/create_workfile.py @@ -9,7 +9,7 @@ class TVPaintWorkfileCreator(TVPaintAutoCreator): label = "Workfile" icon = "fa.file-o" - def apply_settings(self, project_settings, system_settings): + def apply_settings(self, project_settings): plugin_settings = ( project_settings["tvpaint"]["create"]["create_workfile"] ) From 21d5d4cad2705edc8aadc593356a796a14a155e1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Sep 2023 17:08:13 +0200 Subject: [PATCH 251/327] removed system settings usage from traypublisher --- .../hosts/traypublisher/plugins/create/create_movie_batch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_movie_batch.py b/openpype/hosts/traypublisher/plugins/create/create_movie_batch.py index 1bed07f785..3454b6e135 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_movie_batch.py +++ b/openpype/hosts/traypublisher/plugins/create/create_movie_batch.py @@ -36,7 +36,7 @@ class BatchMovieCreator(TrayPublishCreator): # Position batch creator after simple creators order = 110 - def apply_settings(self, project_settings, system_settings): + def apply_settings(self, project_settings): creator_settings = ( project_settings["traypublisher"]["create"]["BatchMovieCreator"] ) From cb3ef02fc377ad375ebf2ccf061da735414d7703 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Sep 2023 17:10:07 +0200 Subject: [PATCH 252/327] fusion creator does not expect system settings --- openpype/hosts/fusion/plugins/create/create_saver.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 04898d0a45..344deffb42 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -250,11 +250,7 @@ class CreateSaver(NewCreator): label="Review", ) - def apply_settings( - self, - project_settings, - system_settings - ): + def apply_settings(self, project_settings): """Method called on initialization of plugin to apply settings.""" # plugin settings From 09d31e81ef01fd94b64d6bcd14dde61960c44a91 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Sep 2023 17:10:26 +0200 Subject: [PATCH 253/327] removed duplicated attribute --- openpype/hosts/fusion/plugins/create/create_saver.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 344deffb42..39edca4de3 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -30,10 +30,6 @@ class CreateSaver(NewCreator): instance_attributes = [ "reviewable" ] - default_variants = [ - "Main", - "Mask" - ] # TODO: This should be renamed together with Nuke so it is aligned temp_rendering_path_template = ( From 3ba7939222c920367a68153a46c8a4ded8684369 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Sep 2023 17:10:40 +0200 Subject: [PATCH 254/327] removed usage of system settings from after effects --- openpype/hosts/aftereffects/plugins/create/create_render.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/aftereffects/plugins/create/create_render.py b/openpype/hosts/aftereffects/plugins/create/create_render.py index dcf424b44f..fbe600ae68 100644 --- a/openpype/hosts/aftereffects/plugins/create/create_render.py +++ b/openpype/hosts/aftereffects/plugins/create/create_render.py @@ -164,7 +164,7 @@ class RenderCreator(Creator): api.get_stub().rename_item(comp_id, new_comp_name) - def apply_settings(self, project_settings, system_settings): + def apply_settings(self, project_settings): plugin_settings = ( project_settings["aftereffects"]["create"]["RenderCreator"] ) From 539120f71861569dfce8e1e2bc39a87fe408f9aa Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Sep 2023 17:11:21 +0200 Subject: [PATCH 255/327] removed usage of system settings from houdini --- openpype/hosts/houdini/api/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/api/plugin.py b/openpype/hosts/houdini/api/plugin.py index 70c837205e..730a627dc3 100644 --- a/openpype/hosts/houdini/api/plugin.py +++ b/openpype/hosts/houdini/api/plugin.py @@ -296,7 +296,7 @@ class HoudiniCreator(NewCreator, HoudiniCreatorBase): """ return [hou.ropNodeTypeCategory()] - def apply_settings(self, project_settings, system_settings): + def apply_settings(self, project_settings): """Method called on initialization of plugin to apply settings.""" settings_name = self.settings_name From cb3e83118b29e48b430da8a5eaf124e928bad3bc Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Sep 2023 17:14:39 +0200 Subject: [PATCH 256/327] maya create plugins do not expecte system settings --- openpype/hosts/maya/api/plugin.py | 2 +- openpype/hosts/maya/plugins/create/create_animation.py | 6 ++---- openpype/hosts/maya/plugins/create/create_render.py | 2 +- .../hosts/maya/plugins/create/create_unreal_skeletalmesh.py | 2 +- .../hosts/maya/plugins/create/create_unreal_staticmesh.py | 2 +- openpype/hosts/maya/plugins/create/create_vrayscene.py | 2 +- 6 files changed, 7 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index 00d6602ef9..3f383fafb8 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -260,7 +260,7 @@ class MayaCreator(NewCreator, MayaCreatorBase): default=True) ] - def apply_settings(self, project_settings, system_settings): + def apply_settings(self, project_settings): """Method called on initialization of plugin to apply settings.""" settings_name = self.settings_name diff --git a/openpype/hosts/maya/plugins/create/create_animation.py b/openpype/hosts/maya/plugins/create/create_animation.py index 214ac18aef..115c73c0d3 100644 --- a/openpype/hosts/maya/plugins/create/create_animation.py +++ b/openpype/hosts/maya/plugins/create/create_animation.py @@ -81,10 +81,8 @@ class CreateAnimation(plugin.MayaHiddenCreator): return defs - def apply_settings(self, project_settings, system_settings): - super(CreateAnimation, self).apply_settings( - project_settings, system_settings - ) + def apply_settings(self, project_settings): + super(CreateAnimation, self).apply_settings(project_settings) # Hardcoding creator to be enabled due to existing settings would # disable the creator causing the creator plugin to not be # discoverable. diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index cc5c1eb205..6266689af4 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -34,7 +34,7 @@ class CreateRenderlayer(plugin.RenderlayerCreator): render_settings = {} @classmethod - def apply_settings(cls, project_settings, system_settings): + def apply_settings(cls, project_settings): cls.render_settings = project_settings["maya"]["RenderSettings"] def create(self, subset_name, instance_data, pre_create_data): diff --git a/openpype/hosts/maya/plugins/create/create_unreal_skeletalmesh.py b/openpype/hosts/maya/plugins/create/create_unreal_skeletalmesh.py index 4e2a99eced..3c9a79156a 100644 --- a/openpype/hosts/maya/plugins/create/create_unreal_skeletalmesh.py +++ b/openpype/hosts/maya/plugins/create/create_unreal_skeletalmesh.py @@ -21,7 +21,7 @@ class CreateUnrealSkeletalMesh(plugin.MayaCreator): # Defined in settings joint_hints = set() - def apply_settings(self, project_settings, system_settings): + def apply_settings(self, project_settings): """Apply project settings to creator""" settings = ( project_settings["maya"]["create"]["CreateUnrealSkeletalMesh"] diff --git a/openpype/hosts/maya/plugins/create/create_unreal_staticmesh.py b/openpype/hosts/maya/plugins/create/create_unreal_staticmesh.py index 3f96d91a54..025b39fa55 100644 --- a/openpype/hosts/maya/plugins/create/create_unreal_staticmesh.py +++ b/openpype/hosts/maya/plugins/create/create_unreal_staticmesh.py @@ -16,7 +16,7 @@ class CreateUnrealStaticMesh(plugin.MayaCreator): # Defined in settings collision_prefixes = [] - def apply_settings(self, project_settings, system_settings): + def apply_settings(self, project_settings): """Apply project settings to creator""" settings = project_settings["maya"]["create"]["CreateUnrealStaticMesh"] self.collision_prefixes = settings["collision_prefixes"] diff --git a/openpype/hosts/maya/plugins/create/create_vrayscene.py b/openpype/hosts/maya/plugins/create/create_vrayscene.py index d601dceb54..2726979d30 100644 --- a/openpype/hosts/maya/plugins/create/create_vrayscene.py +++ b/openpype/hosts/maya/plugins/create/create_vrayscene.py @@ -22,7 +22,7 @@ class CreateVRayScene(plugin.RenderlayerCreator): singleton_node_name = "vraysceneMain" @classmethod - def apply_settings(cls, project_settings, system_settings): + def apply_settings(cls, project_settings): cls.render_settings = project_settings["maya"]["RenderSettings"] def create(self, subset_name, instance_data, pre_create_data): From 9338c163057d6c644bb6e113b51ed05170f8a951 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Sep 2023 17:18:10 +0200 Subject: [PATCH 257/327] photoshop create plugins do not expect system settings --- openpype/hosts/photoshop/plugins/create/create_flatten_image.py | 2 +- openpype/hosts/photoshop/plugins/create/create_image.py | 2 +- openpype/hosts/photoshop/plugins/create/create_review.py | 2 +- openpype/hosts/photoshop/plugins/create/create_workfile.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/create/create_flatten_image.py b/openpype/hosts/photoshop/plugins/create/create_flatten_image.py index 3bc61c8184..9d4189a1a3 100644 --- a/openpype/hosts/photoshop/plugins/create/create_flatten_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_flatten_image.py @@ -98,7 +98,7 @@ class AutoImageCreator(PSAutoCreator): ) ] - def apply_settings(self, project_settings, system_settings): + def apply_settings(self, project_settings): plugin_settings = ( project_settings["photoshop"]["create"]["AutoImageCreator"] ) diff --git a/openpype/hosts/photoshop/plugins/create/create_image.py b/openpype/hosts/photoshop/plugins/create/create_image.py index f3165fca57..8d3ac9f459 100644 --- a/openpype/hosts/photoshop/plugins/create/create_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_image.py @@ -171,7 +171,7 @@ class ImageCreator(Creator): ) ] - def apply_settings(self, project_settings, system_settings): + def apply_settings(self, project_settings): plugin_settings = ( project_settings["photoshop"]["create"]["ImageCreator"] ) diff --git a/openpype/hosts/photoshop/plugins/create/create_review.py b/openpype/hosts/photoshop/plugins/create/create_review.py index 064485d465..63751d94e4 100644 --- a/openpype/hosts/photoshop/plugins/create/create_review.py +++ b/openpype/hosts/photoshop/plugins/create/create_review.py @@ -18,7 +18,7 @@ class ReviewCreator(PSAutoCreator): it will get recreated in next publish either way). """ - def apply_settings(self, project_settings, system_settings): + def apply_settings(self, project_settings): plugin_settings = ( project_settings["photoshop"]["create"]["ReviewCreator"] ) diff --git a/openpype/hosts/photoshop/plugins/create/create_workfile.py b/openpype/hosts/photoshop/plugins/create/create_workfile.py index d498f0549c..1b255de3a3 100644 --- a/openpype/hosts/photoshop/plugins/create/create_workfile.py +++ b/openpype/hosts/photoshop/plugins/create/create_workfile.py @@ -19,7 +19,7 @@ class WorkfileCreator(PSAutoCreator): in next publish automatically). """ - def apply_settings(self, project_settings, system_settings): + def apply_settings(self, project_settings): plugin_settings = ( project_settings["photoshop"]["create"]["WorkfileCreator"] ) From 7441d2617ab118db431b0ce57079ea25ffd8209b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Sep 2023 17:19:30 +0200 Subject: [PATCH 258/327] add missing tvpaint creators --- openpype/hosts/tvpaint/plugins/create/create_render.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render.py b/openpype/hosts/tvpaint/plugins/create/create_render.py index 656ea5d80b..b7a7c208d9 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render.py @@ -387,7 +387,7 @@ class CreateRenderPass(TVPaintCreator): # Settings mark_for_review = True - def apply_settings(self, project_settings, system_settings): + def apply_settings(self, project_settings): plugin_settings = ( project_settings["tvpaint"]["create"]["create_render_pass"] ) @@ -690,7 +690,7 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): group_idx_offset = 10 group_idx_padding = 3 - def apply_settings(self, project_settings, system_settings): + def apply_settings(self, project_settings): plugin_settings = ( project_settings ["tvpaint"] @@ -1029,7 +1029,7 @@ class TVPaintSceneRenderCreator(TVPaintAutoCreator): mark_for_review = True active_on_create = False - def apply_settings(self, project_settings, system_settings): + def apply_settings(self, project_settings): plugin_settings = ( project_settings["tvpaint"]["create"]["create_render_scene"] ) From ce3e9a52401fdd85a6574b6fafc0d23351e69135 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Sep 2023 17:19:39 +0200 Subject: [PATCH 259/327] nuke create plugins do not expect system settings --- openpype/hosts/nuke/api/plugin.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index 6d48c09d60..a0e1525cd0 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -379,11 +379,7 @@ class NukeWriteCreator(NukeCreator): sys.exc_info()[2] ) - def apply_settings( - self, - project_settings, - system_settings - ): + def apply_settings(self, project_settings): """Method called on initialization of plugin to apply settings.""" # plugin settings From 34d7a4f477c109ec419a2048d72cdc657bae7e51 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 1 Sep 2023 17:24:01 +0200 Subject: [PATCH 260/327] removed unnecessary line Co-authored-by: Roy Nieterau --- openpype/lib/attribute_definitions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/lib/attribute_definitions.py b/openpype/lib/attribute_definitions.py index a71709cace..095b096255 100644 --- a/openpype/lib/attribute_definitions.py +++ b/openpype/lib/attribute_definitions.py @@ -482,7 +482,6 @@ class EnumDef(AbstractAttrDef): return copy.deepcopy(self.default) return list(self._item_values.intersection(value)) - def serialize(self): data = super(EnumDef, self).serialize() data["items"] = copy.deepcopy(self.items) From 5de86cb6df5c852b590a83f72d4b3bfe1cdf1ebb Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Sep 2023 17:33:57 +0200 Subject: [PATCH 261/327] do not override scale factor rounding policy if has defined value through env variable --- openpype/tools/utils/lib.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index 2df46c1eae..723e71e7aa 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -170,8 +170,12 @@ def get_openpype_qt_app(): if attr is not None: QtWidgets.QApplication.setAttribute(attr) - if hasattr( - QtWidgets.QApplication, "setHighDpiScaleFactorRoundingPolicy" + policy = os.getenv("QT_SCALE_FACTOR_ROUNDING_POLICY") + if ( + hasattr( + QtWidgets.QApplication, "setHighDpiScaleFactorRoundingPolicy" + ) + and not policy ): QtWidgets.QApplication.setHighDpiScaleFactorRoundingPolicy( QtCore.Qt.HighDpiScaleFactorRoundingPolicy.PassThrough From 88ab56db12f21a6aabf558bf1c9b1ab8471a4011 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Sep 2023 18:16:54 +0200 Subject: [PATCH 262/327] backwards compatibility is kept with waring message --- openpype/pipeline/create/context.py | 16 ++++++---------- openpype/pipeline/create/creator_plugins.py | 19 +++++++++++++++++-- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index e0e2c09f96..f9e3f86652 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -16,7 +16,6 @@ from openpype.settings import ( get_system_settings, get_project_settings ) -from openpype.lib import is_func_signature_supported from openpype.lib.attribute_definitions import ( UnknownDef, serialize_attr_defs, @@ -1799,15 +1798,12 @@ class CreateContext: ).format(creator_class.host_name, self.host_name)) continue - if is_func_signature_supported( - creator_class, project_settings, self, self.headless - ): - creator = creator_class(project_settings, self, self.headless) - else: - # Backwards compatibility to pass system settings to creators - creator = creator_class( - project_settings, system_settings, self, self.headless - ) + creator = creator_class( + project_settings, + system_settings, + self, + self.headless + ) if not creator.enabled: disabled_creators[creator_identifier] = creator diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index b38c1199e6..bd9d1d16c3 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -193,7 +193,9 @@ class BaseCreator: # QUESTION make this required? host_name = None - def __init__(self, project_settings, create_context, headless=False): + def __init__( + self, project_settings, system_settings, create_context, headless=False + ): # Reference to CreateContext self.create_context = create_context self.project_settings = project_settings @@ -202,13 +204,26 @@ class BaseCreator: # - we may use UI inside processing this attribute should be checked self.headless = headless + expect_system_settings = False if is_func_signature_supported( self.apply_settings, project_settings ): self.apply_settings(project_settings) else: + expect_system_settings = True # Backwards compatibility for system settings - self.apply_settings(project_settings, {}) + self.apply_settings(project_settings, system_settings) + + init_overriden = self.__class__.__init__ is not BaseCreator.__init__ + if init_overriden or expect_system_settings: + self.log.warning(( + "WARNING: Source - Create plugin {}." + " System settings argument will not be passed to" + " '__init__' and 'apply_settings' methods in future versions" + " of OpenPype. Planned version to drop the support" + " is 3.15.6 or 3.16.0. Please contact Ynput core team if you" + " need to keep system settings." + ).format(self.__class__.__name__)) def apply_settings(self, project_settings): """Method called on initialization of plugin to apply settings. From 1b8d8e68f2b7e41f3f141a3ad8570b49ae0d7be5 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 1 Sep 2023 18:23:45 +0200 Subject: [PATCH 263/327] Harmony: refresh code for current Deadline (#5493) * Explicitly set Python3 for Harmony OP plugin * Fix method call Without it it would always return True * Explicitly set render instance to farm * Added Harmony 22 executable This plugin might not be necessary anymore for current Harmonies (at least for 22 works original Harmony plugin in DL) * Removed logging * fix - remove explicit disabling of review instance.data["review"] could be False only if review shouldn't be explicitly done. This is not possible in old publisher. TODO must be implemented in NP. --- .../plugins/publish/collect_farm_render.py | 2 +- .../publish/submit_harmony_deadline.py | 2 +- .../HarmonyOpenPype/HarmonyOpenPype.param | 20 ++++++++++++++++++- .../HarmonyOpenPype/HarmonyOpenPype.py | 1 + 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/harmony/plugins/publish/collect_farm_render.py b/openpype/hosts/harmony/plugins/publish/collect_farm_render.py index 5daa93cddb..af825c052a 100644 --- a/openpype/hosts/harmony/plugins/publish/collect_farm_render.py +++ b/openpype/hosts/harmony/plugins/publish/collect_farm_render.py @@ -147,13 +147,13 @@ class CollectFarmRender(publish.AbstractCollectRender): attachTo=False, setMembers=[node], publish=info[4], - review=False, renderer=None, priority=50, name=node.split("/")[1], family="render.farm", families=["render.farm"], + farm=True, resolutionWidth=context.data["resolutionWidth"], resolutionHeight=context.data["resolutionHeight"], diff --git a/openpype/modules/deadline/plugins/publish/submit_harmony_deadline.py b/openpype/modules/deadline/plugins/publish/submit_harmony_deadline.py index 2c37268f04..16e703fc91 100644 --- a/openpype/modules/deadline/plugins/publish/submit_harmony_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_harmony_deadline.py @@ -265,7 +265,7 @@ class HarmonySubmitDeadline( job_info.SecondaryPool = self._instance.data.get("secondaryPool") job_info.ChunkSize = self.chunk_size batch_name = os.path.basename(self._instance.data["source"]) - if is_in_tests: + if is_in_tests(): batch_name += datetime.now().strftime("%d%m%Y%H%M%S") job_info.BatchName = batch_name job_info.Department = self.department diff --git a/openpype/modules/deadline/repository/custom/plugins/HarmonyOpenPype/HarmonyOpenPype.param b/openpype/modules/deadline/repository/custom/plugins/HarmonyOpenPype/HarmonyOpenPype.param index ff2949766c..43a54a464e 100644 --- a/openpype/modules/deadline/repository/custom/plugins/HarmonyOpenPype/HarmonyOpenPype.param +++ b/openpype/modules/deadline/repository/custom/plugins/HarmonyOpenPype/HarmonyOpenPype.param @@ -77,4 +77,22 @@ CategoryOrder=0 Index=4 Label=Harmony 20 Render Executable Description=The path to the Harmony Render executable file used for rendering. Enter alternative paths on separate lines. -Default=c:\Program Files (x86)\Toon Boom Animation\Toon Boom Harmony 20 Premium\win64\bin\HarmonyPremium.exe;/Applications/Toon Boom Harmony 20 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium;/usr/local/ToonBoomAnimation/harmonyPremium_20/lnx86_64/bin/HarmonyPremium \ No newline at end of file +Default=c:\Program Files (x86)\Toon Boom Animation\Toon Boom Harmony 20 Premium\win64\bin\HarmonyPremium.exe;/Applications/Toon Boom Harmony 20 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium;/usr/local/ToonBoomAnimation/harmonyPremium_20/lnx86_64/bin/HarmonyPremium + +[Harmony_RenderExecutable_21] +Type=multilinemultifilename +Category=Render Executables +CategoryOrder=0 +Index=4 +Label=Harmony 21 Render Executable +Description=The path to the Harmony Render executable file used for rendering. Enter alternative paths on separate lines. +Default=c:\Program Files (x86)\Toon Boom Animation\Toon Boom Harmony 21 Premium\win64\bin\HarmonyPremium.exe;/Applications/Toon Boom Harmony 21 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium;/usr/local/ToonBoomAnimation/harmonyPremium_21/lnx86_64/bin/HarmonyPremium + +[Harmony_RenderExecutable_22] +Type=multilinemultifilename +Category=Render Executables +CategoryOrder=0 +Index=4 +Label=Harmony 22 Render Executable +Description=The path to the Harmony Render executable file used for rendering. Enter alternative paths on separate lines. +Default=c:\Program Files (x86)\Toon Boom Animation\Toon Boom Harmony 22 Premium\win64\bin\HarmonyPremium.exe;/Applications/Toon Boom Harmony 22 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium;/usr/local/ToonBoomAnimation/harmonyPremium_22/lnx86_64/bin/HarmonyPremium diff --git a/openpype/modules/deadline/repository/custom/plugins/HarmonyOpenPype/HarmonyOpenPype.py b/openpype/modules/deadline/repository/custom/plugins/HarmonyOpenPype/HarmonyOpenPype.py index 2f6e9cf379..32ed76b58d 100644 --- a/openpype/modules/deadline/repository/custom/plugins/HarmonyOpenPype/HarmonyOpenPype.py +++ b/openpype/modules/deadline/repository/custom/plugins/HarmonyOpenPype/HarmonyOpenPype.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python3 from System import * from System.Diagnostics import * from System.IO import * From 869c6277ff1d89599086169926dfb48673c088c0 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 1 Sep 2023 21:03:09 +0300 Subject: [PATCH 264/327] BigRoy's comment --- .../publish/validate_review_colorspace.py | 40 ++++++++++++------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py index 3f5e5bc354..e5d4756556 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py +++ b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py @@ -37,15 +37,15 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin, if not self.is_active(instance.data): return - invalid = self.get_invalid(instance) - if invalid: + invalid_nodes, message = self.get_invalid_with_message(instance) + if invalid_nodes: raise PublishValidationError( - ("'OCIO Colorspace' parameter is not valid."), + message, title=self.label ) @classmethod - def get_invalid(cls, instance): + def get_invalid_with_message(cls, instance): rop_node = hou.node(instance.data["instance_node"]) if os.getenv("OCIO") is None: @@ -53,26 +53,31 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin, "Default Houdini colorspace is used, " " skipping check.." ) - return + return None, None if rop_node.evalParm("colorcorrect") != 2: # any colorspace settings other than default requires # 'Color Correct' parm to be set to 'OpenColorIO' - rop_node.setParms({"colorcorrect": 2}) - cls.log.debug( - "'Color Correct' parm on '{}' has been set to" - " 'OpenColorIO'".format(rop_node) + error = ( + "'Color Correction' parm on '{}' ROP must be set to" + " 'OpenColorIO'".format(rop_node.path()) ) + return rop_node , error if rop_node.evalParm("ociocolorspace") not in \ hou.Color.ocio_spaces(): - cls.log.error( - "'OCIO Colorspace' value on '{}' is not valid, " - "select a valid option from the dropdown menu." - .format(rop_node) + error = ( + "Invalid value: Colorspace name doesn't exist.\n" + "Check 'OCIO Colorspace' parameter on '{}' ROP" + .format(rop_node.path()) ) - return rop_node + return rop_node, error + + @classmethod + def get_invalid(cls, instance): + nodes, _ = cls.get_invalid_with_message(instance) + return nodes @classmethod def repair(cls, instance): @@ -84,6 +89,13 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin, rop_node = hou.node(instance.data["instance_node"]) + if rop_node.evalParm("colorcorrect") != 2: + rop_node.setParms({"colorcorrect": 2}) + cls.log.debug( + "'Color Correction' parm on '{}' has been set to" + " 'OpenColorIO'".format(rop_node.path()) + ) + # Get default view colorspace name default_view_space = get_default_display_view_colorspace() From 087aca6d8b9377c054e623bc2f12f6b418a69aa0 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 1 Sep 2023 21:04:59 +0300 Subject: [PATCH 265/327] resolve hound --- .../hosts/houdini/plugins/publish/validate_review_colorspace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py index e5d4756556..47370678d0 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py +++ b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py @@ -62,7 +62,7 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin, "'Color Correction' parm on '{}' ROP must be set to" " 'OpenColorIO'".format(rop_node.path()) ) - return rop_node , error + return rop_node, error if rop_node.evalParm("ociocolorspace") not in \ hou.Color.ocio_spaces(): From 0cf8f8f42dc0f3b348c55ee02d7e8cdc784838e2 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 2 Sep 2023 03:24:31 +0000 Subject: [PATCH 266/327] [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 12f797228b..7de6fd752b 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.16.5-nightly.3" +__version__ = "3.16.5-nightly.4" From dc684b65f38ddeb6900343f35a8d005049307798 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 2 Sep 2023 03:25:07 +0000 Subject: [PATCH 267/327] 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 669bf391cd..83a4b6a8d4 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.16.5-nightly.4 - 3.16.5-nightly.3 - 3.16.5-nightly.2 - 3.16.5-nightly.1 @@ -134,7 +135,6 @@ body: - 3.14.9-nightly.1 - 3.14.8 - 3.14.8-nightly.4 - - 3.14.8-nightly.3 validations: required: true - type: dropdown From c2b948d35fea1f0cf2f7146bb0085fc3bd2dfeef Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Mon, 4 Sep 2023 11:09:47 +0300 Subject: [PATCH 268/327] BigRoy's comments --- .../publish/validate_review_colorspace.py | 27 ++++--------------- openpype/pipeline/colorspace.py | 7 +---- openpype/scripts/ocio_wrapper.py | 11 ++++---- 3 files changed, 11 insertions(+), 34 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py index 47370678d0..d457a295c5 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py +++ b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py @@ -37,47 +37,30 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin, if not self.is_active(instance.data): return - invalid_nodes, message = self.get_invalid_with_message(instance) - if invalid_nodes: - raise PublishValidationError( - message, - title=self.label - ) - - @classmethod - def get_invalid_with_message(cls, instance): - - rop_node = hou.node(instance.data["instance_node"]) if os.getenv("OCIO") is None: - cls.log.debug( + self.log.debug( "Default Houdini colorspace is used, " " skipping check.." ) - return None, None + return + rop_node = hou.node(instance.data["instance_node"]) if rop_node.evalParm("colorcorrect") != 2: # any colorspace settings other than default requires # 'Color Correct' parm to be set to 'OpenColorIO' - error = ( + raise PublishValidationError( "'Color Correction' parm on '{}' ROP must be set to" " 'OpenColorIO'".format(rop_node.path()) ) - return rop_node, error if rop_node.evalParm("ociocolorspace") not in \ hou.Color.ocio_spaces(): - error = ( + raise PublishValidationError( "Invalid value: Colorspace name doesn't exist.\n" "Check 'OCIO Colorspace' parameter on '{}' ROP" .format(rop_node.path()) ) - return rop_node, error - - @classmethod - def get_invalid(cls, instance): - nodes, _ = cls.get_invalid_with_message(instance) - return nodes @classmethod def repair(cls, instance): diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 3dd33d0425..e167e18cfb 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -648,15 +648,10 @@ def get_display_view_colorspace_subprocess(config_path, display, view): "--out_path", tmp_json_path, "--display", display, "--view", view - ] log.debug("Executing: {}".format(" ".join(args))) - process_kwargs = { - "logger": log - } - - run_openpype_process(*args, **process_kwargs) + run_openpype_process(*args, logger=log) # return default view colorspace name with open(tmp_json_path, "r") as f: diff --git a/openpype/scripts/ocio_wrapper.py b/openpype/scripts/ocio_wrapper.py index cae6e6975b..2c11bb7eeb 100644 --- a/openpype/scripts/ocio_wrapper.py +++ b/openpype/scripts/ocio_wrapper.py @@ -225,7 +225,7 @@ def get_display_view_colorspace_name(in_path, out_path, display, view): """Aggregate view colorspace name to file. - Wrapper command for processes without acces to OpenColorIO + Wrapper command for processes without access to OpenColorIO Args: in_path (str): config file path string @@ -239,15 +239,14 @@ def get_display_view_colorspace_name(in_path, out_path, --out_path= --display= --view= """ - json_path = Path(out_path) - out_data = _get_display_view_colorspace_name(in_path, - display, view) + display, + view) - with open(json_path, "w") as f: + with open(out_path, "w") as f: json.dump(out_data, f) - print(f"Display view colorspace saved to '{json_path}'") + print(f"Display view colorspace saved to '{out_path}'") if __name__ == '__main__': main() From 115dd54733c83744548643e720b108d00ef1e529 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 4 Sep 2023 10:56:30 +0200 Subject: [PATCH 269/327] fix repaint when custom text changed --- openpype/tools/utils/multiselection_combobox.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/tools/utils/multiselection_combobox.py b/openpype/tools/utils/multiselection_combobox.py index 13b396a059..34361fca17 100644 --- a/openpype/tools/utils/multiselection_combobox.py +++ b/openpype/tools/utils/multiselection_combobox.py @@ -75,11 +75,11 @@ class MultiSelectionComboBox(QtWidgets.QComboBox): def set_placeholder_text(self, text): self._placeholder_text = text + self._update_size_hint() def set_custom_text(self, text): self._custom_text = text - self.update() - self.updateGeometry() + self._update_size_hint() def focusInEvent(self, event): self.focused_in.emit() @@ -266,7 +266,6 @@ class MultiSelectionComboBox(QtWidgets.QComboBox): def _update_size_hint(self): if self._custom_text is not None: self.update() - self.repaint() return self._lines = {} @@ -313,7 +312,6 @@ class MultiSelectionComboBox(QtWidgets.QComboBox): self.update() self.updateGeometry() - self.repaint() def sizeHint(self): value = super(MultiSelectionComboBox, self).sizeHint() From 6b1d060a8133643cebbf92e8c3e1fe93c84fbed9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 4 Sep 2023 11:00:00 +0200 Subject: [PATCH 270/327] updated docstring --- openpype/lib/attribute_definitions.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/lib/attribute_definitions.py b/openpype/lib/attribute_definitions.py index 095b096255..a71d6cc72a 100644 --- a/openpype/lib/attribute_definitions.py +++ b/openpype/lib/attribute_definitions.py @@ -424,7 +424,10 @@ class TextDef(AbstractAttrDef): class EnumDef(AbstractAttrDef): - """Enumeration of single item from items. + """Enumeration of items. + + Enumeration of single item from items. Or list of items if multiselection + is enabled. Args: items (Union[list[str], list[dict[str, Any]]): Items definition that From 0f39ccf0168d8e24ffed0d12287db8a15d99a38f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 4 Sep 2023 11:05:27 +0200 Subject: [PATCH 271/327] Fix - files on representation cannot be single item list (#5545) Further logic expects that single item files will be only 'string' not 'list' (eg. repre["files"] = "abc.exr" not repre["files"] = ["abc.exr"]. This would cause an issue in ExtractReview later. --- .../plugins/publish/validate_expected_and_rendered_files.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py b/openpype/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py index 9f1f7bc518..5d37e7357e 100644 --- a/openpype/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py +++ b/openpype/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py @@ -70,7 +70,10 @@ class ValidateExpectedFiles(pyblish.api.InstancePlugin): # Update the representation expected files self.log.info("Update range from actual job range " "to frame list: {}".format(frame_list)) - repre["files"] = sorted(job_expected_files) + # single item files must be string not list + repre["files"] = (sorted(job_expected_files) + if len(job_expected_files) > 1 else + list(job_expected_files)[0]) # Update the expected files expected_files = job_expected_files From 4ed278c0c885e22fdcf97c9a1b242428fee3ff05 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Mon, 4 Sep 2023 12:18:03 +0300 Subject: [PATCH 272/327] BigRoy's comment --- .../houdini/plugins/publish/validate_review_colorspace.py | 3 +-- openpype/scripts/ocio_wrapper.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py index d457a295c5..545d7b16b1 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py +++ b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py @@ -83,9 +83,8 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin, default_view_space = get_default_display_view_colorspace() rop_node.setParms({"ociocolorspace": default_view_space}) - cls.log.debug( + cls.log.info( "'OCIO Colorspace' parm on '{}' has been set to " "the default view color space '{}'" .format(rop_node, default_view_space) - ) diff --git a/openpype/scripts/ocio_wrapper.py b/openpype/scripts/ocio_wrapper.py index 2c11bb7eeb..40553d30f2 100644 --- a/openpype/scripts/ocio_wrapper.py +++ b/openpype/scripts/ocio_wrapper.py @@ -195,7 +195,7 @@ def _get_display_view_colorspace_name(config_path, display, view): if not config_path.is_file(): raise IOError("Input path should be `config.ocio` file") - config = ocio.Config().CreateFromFile(str(config_path)) + config = ocio.Config.CreateFromFile(str(config_path)) colorspace = config.getDisplayViewColorSpaceName(display, view) return colorspace From 35074e8db892d07bfd43ea3860e660ab10843346 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 4 Sep 2023 11:49:14 +0200 Subject: [PATCH 273/327] Fix versions in deprecated message --- openpype/pipeline/create/creator_plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index bd9d1d16c3..de9cc7cff3 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -221,7 +221,7 @@ class BaseCreator: " System settings argument will not be passed to" " '__init__' and 'apply_settings' methods in future versions" " of OpenPype. Planned version to drop the support" - " is 3.15.6 or 3.16.0. Please contact Ynput core team if you" + " is 3.16.6 or 3.17.0. Please contact Ynput core team if you" " need to keep system settings." ).format(self.__class__.__name__)) From 0c8ad276e409840ce665ecd7c8c3655a545cc500 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 4 Sep 2023 18:02:32 +0800 Subject: [PATCH 274/327] Oscar's and BigRoy's comment respectively on namespace function --- openpype/hosts/max/api/lib.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 034307e72a..8287341456 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -316,7 +316,6 @@ def set_timeline(frameStart, frameEnd): def unique_namespace(namespace, format="%02d", prefix="", suffix="", con_suffix="CON"): - from pymxs import runtime as rt """Return unique namespace Arguments: @@ -336,7 +335,7 @@ def unique_namespace(namespace, format="%02d", def current_namespace(): current = namespace - # When inside a namespace Maya adds no trailing : + # When inside a namespace Max adds no trailing : if not current.endswith(":"): current += ":" return current From df2466b714f86336fad823bd9699aced271e5457 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 4 Sep 2023 12:58:31 +0200 Subject: [PATCH 275/327] default fps for sequence from asset instead of project --- openpype/plugins/publish/collect_sequence_frame_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/collect_sequence_frame_data.py b/openpype/plugins/publish/collect_sequence_frame_data.py index 241e7b9011..6c2bfbf358 100644 --- a/openpype/plugins/publish/collect_sequence_frame_data.py +++ b/openpype/plugins/publish/collect_sequence_frame_data.py @@ -52,5 +52,5 @@ class CollectSequenceFrameData(pyblish.api.InstancePlugin): "frameEnd": repres_frames[-1], "handleStart": 0, "handleEnd": 0, - "fps": instance.context.data["projectEntity"]["data"]["fps"] + "fps": instance.context.data["assetEntity"]["data"]["fps"] } From 378ec74136eba325cba019348cdc69cb90c7d6b6 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 4 Sep 2023 12:59:06 +0200 Subject: [PATCH 276/327] validator for frame range should include Plate family --- .../traypublisher/plugins/publish/validate_frame_ranges.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/traypublisher/plugins/publish/validate_frame_ranges.py b/openpype/hosts/traypublisher/plugins/publish/validate_frame_ranges.py index b962ea464a..09de2d8db2 100644 --- a/openpype/hosts/traypublisher/plugins/publish/validate_frame_ranges.py +++ b/openpype/hosts/traypublisher/plugins/publish/validate_frame_ranges.py @@ -15,7 +15,7 @@ class ValidateFrameRange(OptionalPyblishPluginMixin, label = "Validate Frame Range" hosts = ["traypublisher"] - families = ["render"] + families = ["render", "plate"] order = ValidateContentsOrder optional = True From ab019e312441af63b447f74d8e6119bce0ebb06e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 4 Sep 2023 13:01:29 +0200 Subject: [PATCH 277/327] name of plugin should be more explicit --- ....py => collect_missing_frame_range_asset_entity.py} | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) rename openpype/hosts/traypublisher/plugins/publish/{collect_frame_range_asset_entity.py => collect_missing_frame_range_asset_entity.py} (83%) diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_frame_range_asset_entity.py b/openpype/hosts/traypublisher/plugins/publish/collect_missing_frame_range_asset_entity.py similarity index 83% rename from openpype/hosts/traypublisher/plugins/publish/collect_frame_range_asset_entity.py rename to openpype/hosts/traypublisher/plugins/publish/collect_missing_frame_range_asset_entity.py index c18e10e438..72379ea4e1 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_frame_range_asset_entity.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_missing_frame_range_asset_entity.py @@ -2,16 +2,18 @@ import pyblish.api from openpype.pipeline import OptionalPyblishPluginMixin -class CollectFrameDataFromAssetEntity(pyblish.api.InstancePlugin, - OptionalPyblishPluginMixin): - """Collect Frame Range data From Asset Entity +class CollectMissingFrameDataFromAssetEntity( + pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin +): + """Collect Missing Frame Range data From Asset Entity Frame range data will only be collected if the keys are not yet collected for the instance. """ order = pyblish.api.CollectorOrder + 0.491 - label = "Collect Frame Data From Asset Entity" + label = "Collect Missing Frame Data From Asset Entity" families = ["plate", "pointcache", "vdbcache", "online", "render"] From 646d4f6db2850a3faefb32e5635074ae84cd44c3 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 4 Sep 2023 19:19:39 +0800 Subject: [PATCH 278/327] oscar comment on the import custom attribute data --- openpype/hosts/max/api/pipeline.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/max/api/pipeline.py b/openpype/hosts/max/api/pipeline.py index 161e2bdc7b..695169894f 100644 --- a/openpype/hosts/max/api/pipeline.py +++ b/openpype/hosts/max/api/pipeline.py @@ -197,19 +197,18 @@ def import_custom_attribute_data(container: str, selections: list): rt.addModifier(container, modifier) container.modifiers[0].name = "OP Data" rt.custAttributes.add(container.modifiers[0], attrs) - node_list = [] - sel_list = [] + nodes = {} for i in selections: - node_ref = rt.NodeTransformMonitor(node=i) - node_list.append(node_ref) - sel_list.append(str(i)) + nodes = { + str(i) : rt.NodeTransformMonitor(node=i), + } # Setting the property rt.setProperty( container.modifiers[0].openPypeData, - "all_handles", node_list) + "all_handles", nodes.values()) rt.setProperty( container.modifiers[0].openPypeData, - "sel_list", sel_list) + "sel_list", nodes.keys()) def update_custom_attribute_data(container: str, selections: list): From 4e3d0d7eacc63e2ac4892cecbcefc595905df933 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 4 Sep 2023 19:20:42 +0800 Subject: [PATCH 279/327] hound --- openpype/hosts/max/api/pipeline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/api/pipeline.py b/openpype/hosts/max/api/pipeline.py index 695169894f..d9a66c60f5 100644 --- a/openpype/hosts/max/api/pipeline.py +++ b/openpype/hosts/max/api/pipeline.py @@ -200,7 +200,7 @@ def import_custom_attribute_data(container: str, selections: list): nodes = {} for i in selections: nodes = { - str(i) : rt.NodeTransformMonitor(node=i), + str(i): rt.NodeTransformMonitor(node=i), } # Setting the property rt.setProperty( From 3c911f9f5d2995e866fa7f0b5fe41c6eff100bef Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 4 Sep 2023 13:24:07 +0200 Subject: [PATCH 280/327] removed export maya ass job script --- openpype/scripts/export_maya_ass_job.py | 105 ------------------ openpype/scripts/export_maya_ass_sequence.mel | 67 ----------- 2 files changed, 172 deletions(-) delete mode 100644 openpype/scripts/export_maya_ass_job.py delete mode 100644 openpype/scripts/export_maya_ass_sequence.mel diff --git a/openpype/scripts/export_maya_ass_job.py b/openpype/scripts/export_maya_ass_job.py deleted file mode 100644 index 16e841ce96..0000000000 --- a/openpype/scripts/export_maya_ass_job.py +++ /dev/null @@ -1,105 +0,0 @@ -"""This module is used for command line exporting of ASS files. - -WARNING: -This need to be rewriten to be able use it in Pype 3! -""" - -import os -import argparse -import logging -import subprocess -import platform - -try: - from shutil import which -except ImportError: - # we are in python < 3.3 - def which(command): - path = os.getenv('PATH') - for p in path.split(os.path.pathsep): - p = os.path.join(p, command) - if os.path.exists(p) and os.access(p, os.X_OK): - return p - -handler = logging.basicConfig() -log = logging.getLogger("Publish Image Sequences") -log.setLevel(logging.DEBUG) - -error_format = "Failed {plugin.__name__}: {error} -- {error.traceback}" - - -def __main__(): - parser = argparse.ArgumentParser() - parser.add_argument("--paths", - nargs="*", - default=[], - help="The filepaths to publish. This can be a " - "directory or a path to a .json publish " - "configuration.") - parser.add_argument("--gui", - default=False, - action="store_true", - help="Whether to run Pyblish in GUI mode.") - - parser.add_argument("--pype", help="Pype root") - - kwargs, args = parser.parse_known_args() - - print("Running pype ...") - auto_pype_root = os.path.dirname(os.path.abspath(__file__)) - auto_pype_root = os.path.abspath(auto_pype_root + "../../../../..") - - auto_pype_root = os.environ.get('OPENPYPE_SETUP_PATH') or auto_pype_root - if os.environ.get('OPENPYPE_SETUP_PATH'): - print("Got Pype location from environment: {}".format( - os.environ.get('OPENPYPE_SETUP_PATH'))) - - pype_command = "openpype.ps1" - if platform.system().lower() == "linux": - pype_command = "pype" - elif platform.system().lower() == "windows": - pype_command = "openpype.bat" - - if kwargs.pype: - pype_root = kwargs.pype - else: - # test if pype.bat / pype is in the PATH - # if it is, which() will return its path and we use that. - # if not, we use auto_pype_root path. Caveat of that one is - # that it can be UNC path and that will not work on windows. - - pype_path = which(pype_command) - - if pype_path: - pype_root = os.path.dirname(pype_path) - else: - pype_root = auto_pype_root - - print("Set pype root to: {}".format(pype_root)) - print("Paths: {}".format(kwargs.paths or [os.getcwd()])) - - # paths = kwargs.paths or [os.environ.get("OPENPYPE_METADATA_FILE")] or [os.getcwd()] # noqa - - mayabatch = os.environ.get("AVALON_APP_NAME").replace("maya", "mayabatch") - args = [ - os.path.join(pype_root, pype_command), - "launch", - "--app", - mayabatch, - "-script", - os.path.join(pype_root, "repos", "pype", - "pype", "scripts", "export_maya_ass_sequence.mel") - ] - - print("Pype command: {}".format(" ".join(args))) - # Forcing forwaring the environment because environment inheritance does - # not always work. - # Cast all values in environment to str to be safe - env = {k: str(v) for k, v in os.environ.items()} - exit_code = subprocess.call(args, env=env) - if exit_code != 0: - raise RuntimeError("Publishing failed.") - - -if __name__ == '__main__': - __main__() diff --git a/openpype/scripts/export_maya_ass_sequence.mel b/openpype/scripts/export_maya_ass_sequence.mel deleted file mode 100644 index b3b9a8543e..0000000000 --- a/openpype/scripts/export_maya_ass_sequence.mel +++ /dev/null @@ -1,67 +0,0 @@ -/* - Script to export specified layer as ass files. - -Attributes: - - scene_file (str): Name of the scene to load. - start (int): Start frame. - end (int): End frame. - step (int): Step size. - output_path (str): File output path. - render_layer (str): Name of render layer. - -*/ - -$scene_file=`getenv "OPENPYPE_ASS_EXPORT_SCENE_FILE"`; -$step=`getenv "OPENPYPE_ASS_EXPORT_STEP"`; -$start=`getenv "OPENPYPE_ASS_EXPORT_START"`; -$end=`getenv "OPENPYPE_ASS_EXPORT_END"`; -$file_path=`getenv "OPENPYPE_ASS_EXPORT_OUTPUT"`; -$render_layer = `getenv "OPENPYPE_ASS_EXPORT_RENDER_LAYER"`; - -print("*** ASS Export Plugin\n"); - -if ($scene_file == "") { - print("!!! cannot determine scene file\n"); - quit -a -ex -1; -} - -if ($step == "") { - print("!!! cannot determine step size\n"); - quit -a -ex -1; -} - -if ($start == "") { - print("!!! cannot determine start frame\n"); - quit -a -ex -1; -} - -if ($end == "") { - print("!!! cannot determine end frame\n"); - quit -a -ex -1; -} - -if ($file_path == "") { - print("!!! cannot determine output file\n"); - quit -a -ex -1; -} - -if ($render_layer == "") { - print("!!! cannot determine render layer\n"); - quit -a -ex -1; -} - - -print(">>> Opening Scene [ " + $scene_file + " ]\n"); - -// open scene -file -o -f $scene_file; - -// switch to render layer -print(">>> Switching layer [ "+ $render_layer + " ]\n"); -editRenderLayerGlobals -currentRenderLayer $render_layer; - -// export -print(">>> Exporting to [ " + $file_path + " ]\n"); -arnoldExportAss -mask 255 -sl 1 -ll 1 -bb 1 -sf $start -se $end -b -fs $step; -print("--- Done\n"); From aa9846dcbde067e63aa2089752b76840417870bb Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 4 Sep 2023 13:25:35 +0200 Subject: [PATCH 281/327] removed logic where the script is used --- .../plugins/publish/submit_maya_deadline.py | 53 ------------------- 1 file changed, 53 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py index 34f3905a17..24052f23d5 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -334,12 +334,6 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, payload = self._get_vray_render_payload(payload_data) - elif "assscene" in instance.data["families"]: - self.log.debug("Submitting Arnold .ass standalone render..") - ass_export_payload = self._get_arnold_export_payload(payload_data) - export_job = self.submit(ass_export_payload) - - payload = self._get_arnold_render_payload(payload_data) else: self.log.debug("Submitting MayaBatch render..") payload = self._get_maya_payload(payload_data) @@ -635,53 +629,6 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, return job_info, attr.asdict(plugin_info) - def _get_arnold_export_payload(self, data): - - try: - from openpype.scripts import export_maya_ass_job - except Exception: - raise AssertionError( - "Expected module 'export_maya_ass_job' to be available") - - module_path = export_maya_ass_job.__file__ - if module_path.endswith(".pyc"): - module_path = module_path[: -len(".pyc")] + ".py" - - script = os.path.normpath(module_path) - - job_info = copy.deepcopy(self.job_info) - job_info.Name = self._job_info_label("Export") - - # Force a single frame Python job - job_info.Plugin = "Python" - job_info.Frames = 1 - - renderlayer = self._instance.data["setMembers"] - - # add required env vars for the export script - envs = { - "AVALON_APP_NAME": os.environ.get("AVALON_APP_NAME"), - "OPENPYPE_ASS_EXPORT_RENDER_LAYER": renderlayer, - "OPENPYPE_ASS_EXPORT_SCENE_FILE": self.scene_path, - "OPENPYPE_ASS_EXPORT_OUTPUT": job_info.OutputFilename[0], - "OPENPYPE_ASS_EXPORT_START": int(self._instance.data["frameStartHandle"]), # noqa - "OPENPYPE_ASS_EXPORT_END": int(self._instance.data["frameEndHandle"]), # noqa - "OPENPYPE_ASS_EXPORT_STEP": 1 - } - for key, value in envs.items(): - if not value: - continue - job_info.EnvironmentKeyValue[key] = value - - plugin_info = PythonPluginInfo( - ScriptFile=script, - Version="3.6", - Arguments="", - SingleFrameOnly="True" - ) - - return job_info, attr.asdict(plugin_info) - def _get_vray_render_payload(self, data): # Job Info From 90bfd0a79a8ef5fcee474340594949e44f657ab2 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 4 Sep 2023 13:26:36 +0200 Subject: [PATCH 282/327] Yeti Cache: Include viewport preview settings --- .../plugins/publish/collect_yeti_cache.py | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_yeti_cache.py b/openpype/hosts/maya/plugins/publish/collect_yeti_cache.py index e6b5ca4260..4dcda29050 100644 --- a/openpype/hosts/maya/plugins/publish/collect_yeti_cache.py +++ b/openpype/hosts/maya/plugins/publish/collect_yeti_cache.py @@ -4,12 +4,23 @@ import pyblish.api from openpype.hosts.maya.api import lib -SETTINGS = {"renderDensity", - "renderWidth", - "renderLength", - "increaseRenderBounds", - "imageSearchPath", - "cbId"} + +SETTINGS = { + # Preview + "displayOutput", + "colorR", "colorG", "colorB", + "viewportDensity", + "viewportWidth", + "viewportLength", + # Render attributes + "renderDensity", + "renderWidth", + "renderLength", + "increaseRenderBounds", + "imageSearchPath", + # Pipeline specific + "cbId" +} class CollectYetiCache(pyblish.api.InstancePlugin): @@ -39,10 +50,6 @@ class CollectYetiCache(pyblish.api.InstancePlugin): # Get yeti nodes and their transforms yeti_shapes = cmds.ls(instance, type="pgYetiMaya") for shape in yeti_shapes: - shape_data = {"transform": None, - "name": shape, - "cbId": lib.get_id(shape), - "attrs": None} # Get specific node attributes attr_data = {} @@ -58,9 +65,12 @@ class CollectYetiCache(pyblish.api.InstancePlugin): parent = cmds.listRelatives(shape, parent=True)[0] transform_data = {"name": parent, "cbId": lib.get_id(parent)} - # Store collected data - shape_data["attrs"] = attr_data - shape_data["transform"] = transform_data + shape_data = { + "transform": transform_data, + "name": shape, + "cbId": lib.get_id(shape), + "attrs": attr_data, + } settings["nodes"].append(shape_data) From 26ef5812b7428f792e81de4a2bfa61867e73f834 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 4 Sep 2023 13:42:01 +0200 Subject: [PATCH 283/327] Skip viewport attributes on 'update' but preserve what artist tweaked after initial load --- .../hosts/maya/plugins/load/load_yeti_cache.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/load/load_yeti_cache.py b/openpype/hosts/maya/plugins/load/load_yeti_cache.py index 5cded13d4e..4a11ea9a2c 100644 --- a/openpype/hosts/maya/plugins/load/load_yeti_cache.py +++ b/openpype/hosts/maya/plugins/load/load_yeti_cache.py @@ -15,6 +15,16 @@ from openpype.hosts.maya.api import lib from openpype.hosts.maya.api.pipeline import containerise +# Do not reset these values on update but only apply on first load +# to preserve any potential local overrides +SKIP_UPDATE_ATTRS = { + "displayOutput", + "viewportDensity", + "viewportWidth", + "viewportLength", +} + + def set_attribute(node, attr, value): """Wrapper of set attribute which ignores None values""" if value is None: @@ -205,6 +215,8 @@ class YetiCacheLoader(load.LoaderPlugin): yeti_node = yeti_nodes[0] for attr, value in node_settings["attrs"].items(): + if attr in SKIP_UPDATE_ATTRS: + continue set_attribute(attr, value, yeti_node) cmds.setAttr("{}.representation".format(container_node), @@ -311,7 +323,6 @@ class YetiCacheLoader(load.LoaderPlugin): # Update attributes with defaults attributes = node_settings["attrs"] attributes.update({ - "viewportDensity": 0.1, "verbosity": 2, "fileMode": 1, @@ -321,6 +332,9 @@ class YetiCacheLoader(load.LoaderPlugin): "visibleInRefractions": True }) + if "viewportDensity" not in attributes: + attributes["viewportDensity"] = 0.1 + # Apply attributes to pgYetiMaya node for attr, value in attributes.items(): set_attribute(attr, value, yeti_node) From c2722344e052565413914a6e7118024d03dee745 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 4 Sep 2023 20:38:18 +0800 Subject: [PATCH 284/327] update attribute should be correct --- openpype/hosts/max/api/pipeline.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/max/api/pipeline.py b/openpype/hosts/max/api/pipeline.py index d9a66c60f5..72163f5ecf 100644 --- a/openpype/hosts/max/api/pipeline.py +++ b/openpype/hosts/max/api/pipeline.py @@ -197,19 +197,20 @@ def import_custom_attribute_data(container: str, selections: list): rt.addModifier(container, modifier) container.modifiers[0].name = "OP Data" rt.custAttributes.add(container.modifiers[0], attrs) - nodes = {} + node_list = [] + sel_list = [] for i in selections: - nodes = { - str(i): rt.NodeTransformMonitor(node=i), - } + node_ref = rt.NodeTransformMonitor(node=i) + node_list.append(node_ref) + sel_list.append(str(i)) + # Setting the property rt.setProperty( container.modifiers[0].openPypeData, - "all_handles", nodes.values()) + "all_handles", node_list) rt.setProperty( container.modifiers[0].openPypeData, - "sel_list", nodes.keys()) - + "sel_list", sel_list) def update_custom_attribute_data(container: str, selections: list): """Updating the Openpype/AYON custom parameter built by the creator From 6ef67e3ff928fca53f4f10ea3166474d5b59c8c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Mon, 4 Sep 2023 17:17:15 +0200 Subject: [PATCH 285/327] 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 b7728936b0..ce0835dcc6 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -20,7 +20,7 @@ log = Logger.get_logger(__name__) class CachedData: - remapping = None + remapping = {} allowed_exts = { ext.lstrip(".") for ext in IMAGE_EXTENSIONS.union(VIDEO_EXTENSIONS) } From 2fdaadcdc151918163477f3275b646a7044f43e6 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Mon, 4 Sep 2023 19:50:00 +0300 Subject: [PATCH 286/327] Minikiu comment --- .../hosts/houdini/plugins/publish/validate_review_colorspace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py index 545d7b16b1..61c3a755d0 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py +++ b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py @@ -39,7 +39,7 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin, if os.getenv("OCIO") is None: self.log.debug( - "Default Houdini colorspace is used, " + "Using Houdini's Default Color Management, " " skipping check.." ) return From 167527f2bf1b8fed25f38d84d08f0edca6bb718c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 4 Sep 2023 23:55:41 +0200 Subject: [PATCH 287/327] Fix Houdini not showing outdated containers pop-up on scene open when launching with last workfile argument --- openpype/hosts/houdini/api/pipeline.py | 48 ++++++++++++++++---------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/openpype/hosts/houdini/api/pipeline.py b/openpype/hosts/houdini/api/pipeline.py index 3c325edfa7..c9ae801af5 100644 --- a/openpype/hosts/houdini/api/pipeline.py +++ b/openpype/hosts/houdini/api/pipeline.py @@ -303,6 +303,28 @@ def on_save(): lib.set_id(node, new_id, overwrite=False) +def _show_outdated_content_popup(): + # Get main window + parent = lib.get_main_window() + if parent is None: + log.info("Skipping outdated content pop-up " + "because Houdini window can't be found.") + else: + from openpype.widgets import popup + + # Show outdated pop-up + def _on_show_inventory(): + from openpype.tools.utils import host_tools + host_tools.show_scene_inventory(parent=parent) + + dialog = popup.Popup(parent=parent) + dialog.setWindowTitle("Houdini scene has outdated content") + dialog.setMessage("There are outdated containers in " + "your Houdini scene.") + dialog.on_clicked.connect(_on_show_inventory) + dialog.show() + + def on_open(): if not hou.isUIAvailable(): @@ -316,28 +338,18 @@ def on_open(): lib.validate_fps() if any_outdated_containers(): - from openpype.widgets import popup - - log.warning("Scene has outdated content.") - - # Get main window parent = lib.get_main_window() if parent is None: - log.info("Skipping outdated content pop-up " - "because Houdini window can't be found.") + # When opening Houdini with last workfile on launch the UI hasn't + # initialized yet completely when the `on_open` callback triggers. + # We defer the dialog popup to wait for the UI to become available. + # We assume it will open because `hou.isUIAvailable()` returns True + import hdefereval + hdefereval.executeDeferred(_show_outdated_content_popup) else: + _show_outdated_content_popup() - # Show outdated pop-up - def _on_show_inventory(): - from openpype.tools.utils import host_tools - host_tools.show_scene_inventory(parent=parent) - - dialog = popup.Popup(parent=parent) - dialog.setWindowTitle("Houdini scene has outdated content") - dialog.setMessage("There are outdated containers in " - "your Houdini scene.") - dialog.on_clicked.connect(_on_show_inventory) - dialog.show() + log.warning("Scene has outdated content.") def on_new(): From 7c29aa2f7b72dd0a94a5a3531bc8a65c6d1cd3bb Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 5 Sep 2023 00:00:03 +0200 Subject: [PATCH 288/327] Improve errors e.g. raise PublishValidationError or cosmetics --- .../houdini/plugins/publish/collect_output_node.py | 6 ++++-- .../plugins/publish/validate_animation_settings.py | 3 ++- .../plugins/publish/validate_remote_publish.py | 11 +++++------ .../publish/validate_usd_render_product_names.py | 2 +- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_output_node.py b/openpype/hosts/houdini/plugins/publish/collect_output_node.py index 601ed17b39..0b27678ed0 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_output_node.py +++ b/openpype/hosts/houdini/plugins/publish/collect_output_node.py @@ -1,5 +1,7 @@ import pyblish.api +from openpype.pipeline.publish import KnownPublishError + class CollectOutputSOPPath(pyblish.api.InstancePlugin): """Collect the out node's SOP/COP Path value.""" @@ -58,8 +60,8 @@ class CollectOutputSOPPath(pyblish.api.InstancePlugin): elif node_type == "Redshift_Proxy_Output": out_node = node.parm("RS_archive_sopPath").evalAsNode() else: - raise ValueError( - "ROP node type '%s' is" " not supported." % node_type + raise KnownPublishError( + "ROP node type '{}' is not supported.".format(node_type) ) if not out_node: diff --git a/openpype/hosts/houdini/plugins/publish/validate_animation_settings.py b/openpype/hosts/houdini/plugins/publish/validate_animation_settings.py index 4878738ed3..79387fbef5 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_animation_settings.py +++ b/openpype/hosts/houdini/plugins/publish/validate_animation_settings.py @@ -1,5 +1,6 @@ import pyblish.api +from openpype.pipeline.publish import PublishValidationError from openpype.hosts.houdini.api import lib import hou @@ -30,7 +31,7 @@ class ValidateAnimationSettings(pyblish.api.InstancePlugin): invalid = self.get_invalid(instance) if invalid: - raise RuntimeError( + raise PublishValidationError( "Output settings do no match for '%s'" % instance ) diff --git a/openpype/hosts/houdini/plugins/publish/validate_remote_publish.py b/openpype/hosts/houdini/plugins/publish/validate_remote_publish.py index 4e8e5fc0e8..4f71d79382 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_remote_publish.py +++ b/openpype/hosts/houdini/plugins/publish/validate_remote_publish.py @@ -36,11 +36,11 @@ class ValidateRemotePublishOutNode(pyblish.api.ContextPlugin): if node.parm("shellexec").eval(): self.raise_error("Must not execute in shell") if node.parm("prerender").eval() != cmd: - self.raise_error(("REMOTE_PUBLISH node does not have " - "correct prerender script.")) + self.raise_error("REMOTE_PUBLISH node does not have " + "correct prerender script.") if node.parm("lprerender").eval() != "python": - self.raise_error(("REMOTE_PUBLISH node prerender script " - "type not set to 'python'")) + self.raise_error("REMOTE_PUBLISH node prerender script " + "type not set to 'python'") @classmethod def repair(cls, context): @@ -48,5 +48,4 @@ class ValidateRemotePublishOutNode(pyblish.api.ContextPlugin): lib.create_remote_publish_node(force=True) def raise_error(self, message): - self.log.error(message) - raise PublishValidationError(message, title=self.label) + raise PublishValidationError(message) diff --git a/openpype/hosts/houdini/plugins/publish/validate_usd_render_product_names.py b/openpype/hosts/houdini/plugins/publish/validate_usd_render_product_names.py index 02c44ab94e..1daa96f2b9 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_usd_render_product_names.py +++ b/openpype/hosts/houdini/plugins/publish/validate_usd_render_product_names.py @@ -24,7 +24,7 @@ class ValidateUSDRenderProductNames(pyblish.api.InstancePlugin): if not os.path.isabs(filepath): invalid.append( - "Output file path is not " "absolute path: %s" % filepath + "Output file path is not absolute path: %s" % filepath ) if invalid: From 0e580ad3190f9cfa15243ebde5e99f02e3c95e71 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 5 Sep 2023 00:02:59 +0200 Subject: [PATCH 289/327] Fix invalid import `openpype.action` does not exist + raise KnownPublishError --- .../houdini/plugins/publish/increment_current_file.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/increment_current_file.py b/openpype/hosts/houdini/plugins/publish/increment_current_file.py index 2493b28bc1..3569de7693 100644 --- a/openpype/hosts/houdini/plugins/publish/increment_current_file.py +++ b/openpype/hosts/houdini/plugins/publish/increment_current_file.py @@ -2,7 +2,7 @@ import pyblish.api from openpype.lib import version_up from openpype.pipeline import registered_host -from openpype.action import get_errored_plugins_from_data +from openpype.pipeline.publish import get_errored_plugins_from_context from openpype.hosts.houdini.api import HoudiniHost from openpype.pipeline.publish import KnownPublishError @@ -27,7 +27,7 @@ class IncrementCurrentFile(pyblish.api.ContextPlugin): def process(self, context): - errored_plugins = get_errored_plugins_from_data(context) + errored_plugins = get_errored_plugins_from_context(context) if any( plugin.__name__ == "HoudiniSubmitPublishDeadline" for plugin in errored_plugins @@ -40,9 +40,10 @@ class IncrementCurrentFile(pyblish.api.ContextPlugin): # Filename must not have changed since collecting host = registered_host() # type: HoudiniHost current_file = host.current_file() - assert ( - context.data["currentFile"] == current_file - ), "Collected filename mismatches from current scene name." + if context.data["currentFile"] != current_file: + raise KnownPublishError( + "Collected filename mismatches from current scene name." + ) new_filepath = version_up(current_file) host.save_workfile(new_filepath) From 10dd15444ad9244618e4e89e4a176d3aaaa2ef58 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 5 Sep 2023 09:03:54 +0200 Subject: [PATCH 290/327] Log collected scene version as debug - artists don't care --- openpype/plugins/publish/collect_scene_version.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/collect_scene_version.py b/openpype/plugins/publish/collect_scene_version.py index 70a0aca296..7920c1e82b 100644 --- a/openpype/plugins/publish/collect_scene_version.py +++ b/openpype/plugins/publish/collect_scene_version.py @@ -63,4 +63,6 @@ class CollectSceneVersion(pyblish.api.ContextPlugin): "filename: {}".format(filename)) context.data['version'] = int(version) - self.log.info('Scene Version: %s' % context.data.get('version')) + self.log.debug( + "Collected scene version: {}".format(context.data.get('version')) + ) From 43902c0b118019fc770affc9217a751cd6a98c07 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 5 Sep 2023 09:04:13 +0200 Subject: [PATCH 291/327] Add debug log about the skipped input --- .../publish/collect_input_representations_to_versions.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/plugins/publish/collect_input_representations_to_versions.py b/openpype/plugins/publish/collect_input_representations_to_versions.py index 54a3214647..2b8c745d3d 100644 --- a/openpype/plugins/publish/collect_input_representations_to_versions.py +++ b/openpype/plugins/publish/collect_input_representations_to_versions.py @@ -46,3 +46,10 @@ class CollectInputRepresentationsToVersions(pyblish.api.ContextPlugin): version_id = representation_id_to_version_id.get(repre_id) if version_id: input_versions.append(version_id) + else: + self.log.debug( + "Representation id {} skipped because its version is " + "not found in current project. Likely it is loaded " + "from a library project or uses a deleted " + "representation or version.".format(repre_id) + ) From 97bb0de1a93c92e17be7ff31cc80c3a59cc8300e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 5 Sep 2023 09:04:40 +0200 Subject: [PATCH 292/327] Raise KnownPublishError --- openpype/plugins/publish/extract_review_slate.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/openpype/plugins/publish/extract_review_slate.py b/openpype/plugins/publish/extract_review_slate.py index 8b1a06b6b8..2ff7fbe7fa 100644 --- a/openpype/plugins/publish/extract_review_slate.py +++ b/openpype/plugins/publish/extract_review_slate.py @@ -14,10 +14,13 @@ from openpype.lib import ( get_ffmpeg_codec_args, get_ffmpeg_format_args, ) -from openpype.pipeline import publish +from openpype.pipeline.publish import ( + Extractor, + KnownPublishError +) -class ExtractReviewSlate(publish.Extractor): +class ExtractReviewSlate(Extractor): """ Will add slate frame at the start of the video files """ @@ -93,9 +96,10 @@ class ExtractReviewSlate(publish.Extractor): # Raise exception of any stream didn't define input resolution if input_width is None: - raise AssertionError(( + raise KnownPublishError( "FFprobe couldn't read resolution from input file: \"{}\"" - ).format(input_path)) + .format(input_path) + ) ( audio_codec, From 84960826a6250897e262f3c468aadc5a94d23c1a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 5 Sep 2023 09:05:07 +0200 Subject: [PATCH 293/327] Tweak grammar + log what representations got created --- openpype/plugins/publish/integrate.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 0c18ab4466..7e48155b9e 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -306,7 +306,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): # increase if the file transaction takes a long time. op_session.commit() - self.log.info("Subset '{subset[name]}' and Version {version[name]} " + self.log.info("Subset '{subset[name]}' version {version[name]} " "written to database..".format(subset=subset, version=version)) @@ -392,8 +392,13 @@ class IntegrateAsset(pyblish.api.InstancePlugin): p["representation"]["_id"]: p for p in prepared_representations } - self.log.info("Registered {} representations" - "".format(len(prepared_representations))) + self.log.info( + "Registered {} representations: {}".format( + len(prepared_representations), + ", ".join(p["representation"]["name"] + for p in prepared_representations) + ) + ) def prepare_subset(self, instance, op_session, project_name): asset_doc = instance.data["assetEntity"] From 4749ac80400346fd4b8ac27f3de4346ba526ba25 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 5 Sep 2023 09:05:40 +0200 Subject: [PATCH 294/327] Raise PublishValidationError --- openpype/plugins/publish/validate_file_saved.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/validate_file_saved.py b/openpype/plugins/publish/validate_file_saved.py index 448eaccf57..94aadc9358 100644 --- a/openpype/plugins/publish/validate_file_saved.py +++ b/openpype/plugins/publish/validate_file_saved.py @@ -1,5 +1,7 @@ import pyblish.api +from openpype.pipeline.publish import PublishValidationError + class ValidateCurrentSaveFile(pyblish.api.ContextPlugin): """File must be saved before publishing""" @@ -12,4 +14,4 @@ class ValidateCurrentSaveFile(pyblish.api.ContextPlugin): current_file = context.data["currentFile"] if not current_file: - raise RuntimeError("File not saved") + raise PublishValidationError("File not saved") From 68ab2fc934e6a11995884ad1472cb04567f7c271 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 5 Sep 2023 09:06:07 +0200 Subject: [PATCH 295/327] Raise PublishValidationError --- .../plugins/publish/validate_filesequences.py | 5 ++++- openpype/plugins/publish/validate_intent.py | 10 ++++------ .../plugins/publish/validate_publish_dir.py | 17 +++++++++-------- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/openpype/plugins/publish/validate_filesequences.py b/openpype/plugins/publish/validate_filesequences.py index 8a877d79bb..0ac281022d 100644 --- a/openpype/plugins/publish/validate_filesequences.py +++ b/openpype/plugins/publish/validate_filesequences.py @@ -1,5 +1,7 @@ import pyblish.api +from openpype.pipeline.publish import PublishValidationError + class ValidateFileSequences(pyblish.api.ContextPlugin): """Validates whether any file sequences were collected.""" @@ -10,4 +12,5 @@ class ValidateFileSequences(pyblish.api.ContextPlugin): label = "Validate File Sequences" def process(self, context): - assert context, "Nothing collected." + if not context: + raise PublishValidationError("Nothing collected.") diff --git a/openpype/plugins/publish/validate_intent.py b/openpype/plugins/publish/validate_intent.py index 23d57bb2b7..832c7cc0a1 100644 --- a/openpype/plugins/publish/validate_intent.py +++ b/openpype/plugins/publish/validate_intent.py @@ -1,7 +1,7 @@ -import os import pyblish.api from openpype.lib import filter_profiles +from openpype.pipeline.publish import PublishValidationError class ValidateIntent(pyblish.api.ContextPlugin): @@ -51,12 +51,10 @@ class ValidateIntent(pyblish.api.ContextPlugin): )) return - msg = ( - "Please make sure that you select the intent of this publish." - ) - intent = context.data.get("intent") or {} self.log.debug(str(intent)) intent_value = intent.get("value") if not intent_value: - raise AssertionError(msg) + raise PublishValidationError( + "Please make sure that you select the intent of this publish." + ) diff --git a/openpype/plugins/publish/validate_publish_dir.py b/openpype/plugins/publish/validate_publish_dir.py index ad5fd34434..0eb93da583 100644 --- a/openpype/plugins/publish/validate_publish_dir.py +++ b/openpype/plugins/publish/validate_publish_dir.py @@ -47,15 +47,16 @@ class ValidatePublishDir(pyblish.api.InstancePlugin): # original_dirname must be convertable to rootless path # in other case it is path inside of root folder for the project success, _ = anatomy.find_root_template_from_path(original_dirname) - - formatting_data = { - "original_dirname": original_dirname, - } - msg = "Path '{}' not in project folder.".format(original_dirname) + \ - " Please publish from inside of project folder." if not success: - raise PublishXmlValidationError(self, msg, key="not_in_dir", - formatting_data=formatting_data) + raise PublishXmlValidationError( + plugin=self, + message=( + "Path '{}' not in project folder. Please publish from " + "inside of project folder.".format(original_dirname) + ), + key="not_in_dir", + formatting_data={"original_dirname": original_dirname} + ) def _get_template_name_from_instance(self, instance): """Find template which will be used during integration.""" From d46f610e4d110a504991c9669fef644ec7e5c747 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 5 Sep 2023 11:21:46 +0200 Subject: [PATCH 296/327] removed switch_ui script from fusion with related script in 'scripts' --- .../deploy/Scripts/Comp/OpenPype/switch_ui.py | 200 --------------- openpype/scripts/fusion_switch_shot.py | 241 ------------------ 2 files changed, 441 deletions(-) delete mode 100644 openpype/hosts/fusion/deploy/Scripts/Comp/OpenPype/switch_ui.py delete mode 100644 openpype/scripts/fusion_switch_shot.py diff --git a/openpype/hosts/fusion/deploy/Scripts/Comp/OpenPype/switch_ui.py b/openpype/hosts/fusion/deploy/Scripts/Comp/OpenPype/switch_ui.py deleted file mode 100644 index 87322235f5..0000000000 --- a/openpype/hosts/fusion/deploy/Scripts/Comp/OpenPype/switch_ui.py +++ /dev/null @@ -1,200 +0,0 @@ -import os -import sys -import glob -import logging - -from qtpy import QtWidgets, QtCore - -import qtawesome as qta - -from openpype.client import get_assets -from openpype import style -from openpype.pipeline import ( - install_host, - get_current_project_name, -) -from openpype.hosts.fusion import api -from openpype.pipeline.context_tools import get_workdir_from_session - -log = logging.getLogger("Fusion Switch Shot") - - -class App(QtWidgets.QWidget): - - def __init__(self, parent=None): - - ################################################ - # |---------------------| |------------------| # - # |Comp | |Asset | # - # |[..][ v]| |[ v]| # - # |---------------------| |------------------| # - # | Update existing comp [ ] | # - # |------------------------------------------| # - # | Switch | # - # |------------------------------------------| # - ################################################ - - QtWidgets.QWidget.__init__(self, parent) - - layout = QtWidgets.QVBoxLayout() - - # Comp related input - comp_hlayout = QtWidgets.QHBoxLayout() - comp_label = QtWidgets.QLabel("Comp file") - comp_label.setFixedWidth(50) - comp_box = QtWidgets.QComboBox() - - button_icon = qta.icon("fa.folder", color="white") - open_from_dir = QtWidgets.QPushButton() - open_from_dir.setIcon(button_icon) - - comp_box.setFixedHeight(25) - open_from_dir.setFixedWidth(25) - open_from_dir.setFixedHeight(25) - - comp_hlayout.addWidget(comp_label) - comp_hlayout.addWidget(comp_box) - comp_hlayout.addWidget(open_from_dir) - - # Asset related input - asset_hlayout = QtWidgets.QHBoxLayout() - asset_label = QtWidgets.QLabel("Shot") - asset_label.setFixedWidth(50) - - asset_box = QtWidgets.QComboBox() - asset_box.setLineEdit(QtWidgets.QLineEdit()) - asset_box.setFixedHeight(25) - - refresh_icon = qta.icon("fa.refresh", color="white") - refresh_btn = QtWidgets.QPushButton() - refresh_btn.setIcon(refresh_icon) - - asset_box.setFixedHeight(25) - refresh_btn.setFixedWidth(25) - refresh_btn.setFixedHeight(25) - - asset_hlayout.addWidget(asset_label) - asset_hlayout.addWidget(asset_box) - asset_hlayout.addWidget(refresh_btn) - - # Options - options = QtWidgets.QHBoxLayout() - options.setAlignment(QtCore.Qt.AlignLeft) - - current_comp_check = QtWidgets.QCheckBox() - current_comp_check.setChecked(True) - current_comp_label = QtWidgets.QLabel("Use current comp") - - options.addWidget(current_comp_label) - options.addWidget(current_comp_check) - - accept_btn = QtWidgets.QPushButton("Switch") - - layout.addLayout(options) - layout.addLayout(comp_hlayout) - layout.addLayout(asset_hlayout) - layout.addWidget(accept_btn) - - self._open_from_dir = open_from_dir - self._comps = comp_box - self._assets = asset_box - self._use_current = current_comp_check - self._accept_btn = accept_btn - self._refresh_btn = refresh_btn - - self.setWindowTitle("Fusion Switch Shot") - self.setLayout(layout) - - self.resize(260, 140) - self.setMinimumWidth(260) - self.setFixedHeight(140) - - self.connections() - - # Update ui to correct state - self._on_use_current_comp() - self._refresh() - - def connections(self): - self._use_current.clicked.connect(self._on_use_current_comp) - self._open_from_dir.clicked.connect(self._on_open_from_dir) - self._refresh_btn.clicked.connect(self._refresh) - self._accept_btn.clicked.connect(self._on_switch) - - def _on_use_current_comp(self): - state = self._use_current.isChecked() - self._open_from_dir.setEnabled(not state) - self._comps.setEnabled(not state) - - def _on_open_from_dir(self): - - start_dir = get_workdir_from_session() - comp_file, _ = QtWidgets.QFileDialog.getOpenFileName( - self, "Choose comp", start_dir) - - if not comp_file: - return - - # Create completer - self.populate_comp_box([comp_file]) - self._refresh() - - def _refresh(self): - # Clear any existing items - self._assets.clear() - - asset_names = self.collect_asset_names() - completer = QtWidgets.QCompleter(asset_names) - - self._assets.setCompleter(completer) - self._assets.addItems(asset_names) - - def _on_switch(self): - - if not self._use_current.isChecked(): - file_name = self._comps.itemData(self._comps.currentIndex()) - else: - comp = api.get_current_comp() - file_name = comp.GetAttrs("COMPS_FileName") - - asset = self._assets.currentText() - - import colorbleed.scripts.fusion_switch_shot as switch_shot - switch_shot.switch(asset_name=asset, filepath=file_name, new=True) - - def collect_slap_comps(self, directory): - items = glob.glob("{}/*.comp".format(directory)) - return items - - def collect_asset_names(self): - project_name = get_current_project_name() - asset_docs = get_assets(project_name, fields=["name"]) - asset_names = { - asset_doc["name"] - for asset_doc in asset_docs - } - return list(asset_names) - - def populate_comp_box(self, files): - """Ensure we display the filename only but the path is stored as well - - Args: - files (list): list of full file path [path/to/item/item.ext,] - - Returns: - None - """ - - for f in files: - filename = os.path.basename(f) - self._comps.addItem(filename, userData=f) - - -if __name__ == '__main__': - install_host(api) - - app = QtWidgets.QApplication(sys.argv) - window = App() - window.setStyleSheet(style.load_stylesheet()) - window.show() - sys.exit(app.exec_()) diff --git a/openpype/scripts/fusion_switch_shot.py b/openpype/scripts/fusion_switch_shot.py deleted file mode 100644 index 1cc728226f..0000000000 --- a/openpype/scripts/fusion_switch_shot.py +++ /dev/null @@ -1,241 +0,0 @@ -import os -import re -import sys -import logging - -from openpype.client import get_asset_by_name, get_versions - -# Pipeline imports -from openpype.hosts.fusion import api -import openpype.hosts.fusion.api.lib as fusion_lib - -# Config imports -from openpype.lib import version_up -from openpype.pipeline import ( - install_host, - registered_host, - legacy_io, - get_current_project_name, -) - -from openpype.pipeline.context_tools import get_workdir_from_session -from openpype.pipeline.version_start import get_versioning_start - -log = logging.getLogger("Update Slap Comp") - - -def _format_version_folder(folder): - """Format a version folder based on the filepath - - Args: - folder: file path to a folder - - Returns: - str: new version folder name - """ - - new_version = get_versioning_start( - get_current_project_name(), - "fusion", - family="workfile" - ) - if os.path.isdir(folder): - re_version = re.compile(r"v\d+$") - versions = [i for i in os.listdir(folder) if os.path.isdir(i) - and re_version.match(i)] - if versions: - # ensure the "v" is not included - new_version = int(max(versions)[1:]) + 1 - - version_folder = "v{:03d}".format(new_version) - - return version_folder - - -def _get_fusion_instance(): - fusion = getattr(sys.modules["__main__"], "fusion", None) - if fusion is None: - try: - # Support for FuScript.exe, BlackmagicFusion module for py2 only - import BlackmagicFusion as bmf - fusion = bmf.scriptapp("Fusion") - except ImportError: - raise RuntimeError("Could not find a Fusion instance") - return fusion - - -def _format_filepath(session): - - project = session["AVALON_PROJECT"] - asset = session["AVALON_ASSET"] - - # Save updated slap comp - work_path = get_workdir_from_session(session) - walk_to_dir = os.path.join(work_path, "scenes", "slapcomp") - slapcomp_dir = os.path.abspath(walk_to_dir) - - # Ensure destination exists - if not os.path.isdir(slapcomp_dir): - log.warning("Folder did not exist, creating folder structure") - os.makedirs(slapcomp_dir) - - # Compute output path - new_filename = "{}_{}_slapcomp_v001.comp".format(project, asset) - new_filepath = os.path.join(slapcomp_dir, new_filename) - - # Create new unqiue filepath - if os.path.exists(new_filepath): - new_filepath = version_up(new_filepath) - - return new_filepath - - -def _update_savers(comp, session): - """Update all savers of the current comp to ensure the output is correct - - Args: - comp (object): current comp instance - session (dict): the current Avalon session - - Returns: - None - """ - - new_work = get_workdir_from_session(session) - renders = os.path.join(new_work, "renders") - version_folder = _format_version_folder(renders) - renders_version = os.path.join(renders, version_folder) - - comp.Print("New renders to: %s\n" % renders) - - with api.comp_lock_and_undo_chunk(comp): - savers = comp.GetToolList(False, "Saver").values() - for saver in savers: - filepath = saver.GetAttrs("TOOLST_Clip_Name")[1.0] - filename = os.path.basename(filepath) - new_path = os.path.join(renders_version, filename) - saver["Clip"] = new_path - - -def update_frame_range(comp, representations): - """Update the frame range of the comp and render length - - The start and end frame are based on the lowest start frame and the highest - end frame - - Args: - comp (object): current focused comp - representations (list) collection of dicts - - Returns: - None - - """ - - version_ids = [r["parent"] for r in representations] - project_name = get_current_project_name() - versions = list(get_versions(project_name, version_ids=version_ids)) - - start = min(v["data"]["frameStart"] for v in versions) - end = max(v["data"]["frameEnd"] for v in versions) - - fusion_lib.update_frame_range(start, end, comp=comp) - - -def switch(asset_name, filepath=None, new=True): - """Switch the current containers of the file to the other asset (shot) - - Args: - filepath (str): file path of the comp file - asset_name (str): name of the asset (shot) - new (bool): Save updated comp under a different name - - Returns: - comp path (str): new filepath of the updated comp - - """ - - # If filepath provided, ensure it is valid absolute path - if filepath is not None: - if not os.path.isabs(filepath): - filepath = os.path.abspath(filepath) - - assert os.path.exists(filepath), "%s must exist " % filepath - - # Assert asset name exists - # It is better to do this here then to wait till switch_shot does it - project_name = get_current_project_name() - asset = get_asset_by_name(project_name, asset_name) - assert asset, "Could not find '%s' in the database" % asset_name - - # Go to comp - if not filepath: - current_comp = api.get_current_comp() - assert current_comp is not None, "Could not find current comp" - else: - fusion = _get_fusion_instance() - current_comp = fusion.LoadComp(filepath, quiet=True) - assert current_comp is not None, "Fusion could not load '%s'" % filepath - - host = registered_host() - containers = list(host.ls()) - assert containers, "Nothing to update" - - representations = [] - for container in containers: - try: - representation = fusion_lib.switch_item(container, - asset_name=asset_name) - representations.append(representation) - except Exception as e: - current_comp.Print("Error in switching! %s\n" % e.message) - - message = "Switched %i Loaders of the %i\n" % (len(representations), - len(containers)) - current_comp.Print(message) - - # Build the session to switch to - switch_to_session = legacy_io.Session.copy() - switch_to_session["AVALON_ASSET"] = asset['name'] - - if new: - comp_path = _format_filepath(switch_to_session) - - # Update savers output based on new session - _update_savers(current_comp, switch_to_session) - else: - comp_path = version_up(filepath) - - current_comp.Print(comp_path) - - current_comp.Print("\nUpdating frame range") - update_frame_range(current_comp, representations) - - current_comp.Save(comp_path) - - return comp_path - - -if __name__ == '__main__': - - import argparse - - parser = argparse.ArgumentParser(description="Switch to a shot within an" - "existing comp file") - - parser.add_argument("--file_path", - type=str, - default=True, - help="File path of the comp to use") - - parser.add_argument("--asset_name", - type=str, - default=True, - help="Name of the asset (shot) to switch") - - args, unknown = parser.parse_args() - - install_host(api) - switch(args.asset_name, args.file_path) - - sys.exit(0) From 2b8ec005fcc39f54e5a9f14a0352f98f6997034e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 5 Sep 2023 11:27:31 +0200 Subject: [PATCH 297/327] removed remaining scripts after discussion with @BigRoy --- .../32bit/backgrounds_selected_to32bit.py | 16 -------- .../OpenPype/32bit/backgrounds_to32bit.py | 16 -------- .../32bit/loaders_selected_to32bit.py | 16 -------- .../Comp/OpenPype/32bit/loaders_to32bit.py | 16 -------- .../Comp/OpenPype/update_loader_ranges.py | 40 ------------------- 5 files changed, 104 deletions(-) delete mode 100644 openpype/hosts/fusion/deploy/Scripts/Comp/OpenPype/32bit/backgrounds_selected_to32bit.py delete mode 100644 openpype/hosts/fusion/deploy/Scripts/Comp/OpenPype/32bit/backgrounds_to32bit.py delete mode 100644 openpype/hosts/fusion/deploy/Scripts/Comp/OpenPype/32bit/loaders_selected_to32bit.py delete mode 100644 openpype/hosts/fusion/deploy/Scripts/Comp/OpenPype/32bit/loaders_to32bit.py delete mode 100644 openpype/hosts/fusion/deploy/Scripts/Comp/OpenPype/update_loader_ranges.py diff --git a/openpype/hosts/fusion/deploy/Scripts/Comp/OpenPype/32bit/backgrounds_selected_to32bit.py b/openpype/hosts/fusion/deploy/Scripts/Comp/OpenPype/32bit/backgrounds_selected_to32bit.py deleted file mode 100644 index 1a0a9911ea..0000000000 --- a/openpype/hosts/fusion/deploy/Scripts/Comp/OpenPype/32bit/backgrounds_selected_to32bit.py +++ /dev/null @@ -1,16 +0,0 @@ -from openpype.hosts.fusion.api import ( - comp_lock_and_undo_chunk, - get_current_comp -) - - -def main(): - comp = get_current_comp() - """Set all selected backgrounds to 32 bit""" - with comp_lock_and_undo_chunk(comp, 'Selected Backgrounds to 32bit'): - tools = comp.GetToolList(True, "Background").values() - for tool in tools: - tool.Depth = 5 - - -main() diff --git a/openpype/hosts/fusion/deploy/Scripts/Comp/OpenPype/32bit/backgrounds_to32bit.py b/openpype/hosts/fusion/deploy/Scripts/Comp/OpenPype/32bit/backgrounds_to32bit.py deleted file mode 100644 index c2eea505e5..0000000000 --- a/openpype/hosts/fusion/deploy/Scripts/Comp/OpenPype/32bit/backgrounds_to32bit.py +++ /dev/null @@ -1,16 +0,0 @@ -from openpype.hosts.fusion.api import ( - comp_lock_and_undo_chunk, - get_current_comp -) - - -def main(): - comp = get_current_comp() - """Set all backgrounds to 32 bit""" - with comp_lock_and_undo_chunk(comp, 'Backgrounds to 32bit'): - tools = comp.GetToolList(False, "Background").values() - for tool in tools: - tool.Depth = 5 - - -main() diff --git a/openpype/hosts/fusion/deploy/Scripts/Comp/OpenPype/32bit/loaders_selected_to32bit.py b/openpype/hosts/fusion/deploy/Scripts/Comp/OpenPype/32bit/loaders_selected_to32bit.py deleted file mode 100644 index 2118767f4d..0000000000 --- a/openpype/hosts/fusion/deploy/Scripts/Comp/OpenPype/32bit/loaders_selected_to32bit.py +++ /dev/null @@ -1,16 +0,0 @@ -from openpype.hosts.fusion.api import ( - comp_lock_and_undo_chunk, - get_current_comp -) - - -def main(): - comp = get_current_comp() - """Set all selected loaders to 32 bit""" - with comp_lock_and_undo_chunk(comp, 'Selected Loaders to 32bit'): - tools = comp.GetToolList(True, "Loader").values() - for tool in tools: - tool.Depth = 5 - - -main() diff --git a/openpype/hosts/fusion/deploy/Scripts/Comp/OpenPype/32bit/loaders_to32bit.py b/openpype/hosts/fusion/deploy/Scripts/Comp/OpenPype/32bit/loaders_to32bit.py deleted file mode 100644 index 7dd1f66a5e..0000000000 --- a/openpype/hosts/fusion/deploy/Scripts/Comp/OpenPype/32bit/loaders_to32bit.py +++ /dev/null @@ -1,16 +0,0 @@ -from openpype.hosts.fusion.api import ( - comp_lock_and_undo_chunk, - get_current_comp -) - - -def main(): - comp = get_current_comp() - """Set all loaders to 32 bit""" - with comp_lock_and_undo_chunk(comp, 'Loaders to 32bit'): - tools = comp.GetToolList(False, "Loader").values() - for tool in tools: - tool.Depth = 5 - - -main() diff --git a/openpype/hosts/fusion/deploy/Scripts/Comp/OpenPype/update_loader_ranges.py b/openpype/hosts/fusion/deploy/Scripts/Comp/OpenPype/update_loader_ranges.py deleted file mode 100644 index 3d2d1ecfa6..0000000000 --- a/openpype/hosts/fusion/deploy/Scripts/Comp/OpenPype/update_loader_ranges.py +++ /dev/null @@ -1,40 +0,0 @@ -"""Forces Fusion to 'retrigger' the Loader to update. - -Warning: - This might change settings like 'Reverse', 'Loop', trims and other - settings of the Loader. So use this at your own risk. - -""" -from openpype.hosts.fusion.api.pipeline import ( - get_current_comp, - comp_lock_and_undo_chunk -) - - -def update_loader_ranges(): - comp = get_current_comp() - with comp_lock_and_undo_chunk(comp, "Reload clip time ranges"): - tools = comp.GetToolList(True, "Loader").values() - for tool in tools: - - # Get tool attributes - tool_a = tool.GetAttrs() - clipTable = tool_a['TOOLST_Clip_Name'] - altclipTable = tool_a['TOOLST_AltClip_Name'] - startTime = tool_a['TOOLNT_Clip_Start'] - old_global_in = tool.GlobalIn[comp.CurrentTime] - - # Reapply - for index, _ in clipTable.items(): - time = startTime[index] - tool.Clip[time] = tool.Clip[time] - - for index, _ in altclipTable.items(): - time = startTime[index] - tool.ProxyFilename[time] = tool.ProxyFilename[time] - - tool.GlobalIn[comp.CurrentTime] = old_global_in - - -if __name__ == '__main__': - update_loader_ranges() From bde5a560a207dfb28839cb336b30fb59bb0762f0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 5 Sep 2023 11:28:25 +0200 Subject: [PATCH 298/327] Removed ';OpenPype:Scripts' from prefs file --- openpype/hosts/fusion/deploy/fusion_shared.prefs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/deploy/fusion_shared.prefs b/openpype/hosts/fusion/deploy/fusion_shared.prefs index b379ea7c66..93b08aa886 100644 --- a/openpype/hosts/fusion/deploy/fusion_shared.prefs +++ b/openpype/hosts/fusion/deploy/fusion_shared.prefs @@ -5,7 +5,7 @@ Global = { Map = { ["OpenPype:"] = "$(OPENPYPE_FUSION)/deploy", ["Config:"] = "UserPaths:Config;OpenPype:Config", - ["Scripts:"] = "UserPaths:Scripts;Reactor:System/Scripts;OpenPype:Scripts", + ["Scripts:"] = "UserPaths:Scripts;Reactor:System/Scripts", }, }, Script = { From 564cb31cbe700aef687f6b11969eb530973e60d9 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 5 Sep 2023 12:05:40 +0200 Subject: [PATCH 299/327] Fix base `Extractor` being picked up as a plug-in to process --- openpype/plugins/publish/extract_review_slate.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/openpype/plugins/publish/extract_review_slate.py b/openpype/plugins/publish/extract_review_slate.py index 2ff7fbe7fa..d89fbb90c4 100644 --- a/openpype/plugins/publish/extract_review_slate.py +++ b/openpype/plugins/publish/extract_review_slate.py @@ -14,13 +14,11 @@ from openpype.lib import ( get_ffmpeg_codec_args, get_ffmpeg_format_args, ) -from openpype.pipeline.publish import ( - Extractor, - KnownPublishError -) +from openpype.pipeline import publish +from openpype.pipeline.publish import KnownPublishError -class ExtractReviewSlate(Extractor): +class ExtractReviewSlate(publish.Extractor): """ Will add slate frame at the start of the video files """ From e2c3a0f5be2be18ab71cfbd65632f66f1a8b218e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 5 Sep 2023 17:33:25 +0200 Subject: [PATCH 300/327] better check of overriden '__init__' method --- openpype/pipeline/create/creator_plugins.py | 30 ++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index de9cc7cff3..ab0d36e67e 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -214,7 +214,7 @@ class BaseCreator: # Backwards compatibility for system settings self.apply_settings(project_settings, system_settings) - init_overriden = self.__class__.__init__ is not BaseCreator.__init__ + init_overriden = self._method_is_overriden("__init__") if init_overriden or expect_system_settings: self.log.warning(( "WARNING: Source - Create plugin {}." @@ -225,6 +225,19 @@ class BaseCreator: " need to keep system settings." ).format(self.__class__.__name__)) + def _method_is_overriden(self, method_name): + """Check if method is overriden on objects class. + + Implemented for deprecation warning validation on init. + + Returns: + bool: True if method is overriden on objects class. + """ + + cls_method = getattr(BaseCreator, method_name) + obj_method = getattr(self.__class__, method_name) + return cls_method is not obj_method + def apply_settings(self, project_settings): """Method called on initialization of plugin to apply settings. @@ -578,6 +591,11 @@ class Creator(BaseCreator): ) super(Creator, self).__init__(*args, **kwargs) + def _method_is_overriden(self, method_name): + cls_method = getattr(Creator, method_name) + obj_method = getattr(self.__class__, method_name) + return cls_method is not obj_method + @property def show_order(self): """Order in which is creator shown in UI. @@ -720,6 +738,11 @@ class HiddenCreator(BaseCreator): def create(self, instance_data, source_data): pass + def _method_is_overriden(self, method_name): + cls_method = getattr(HiddenCreator, method_name) + obj_method = getattr(self.__class__, method_name) + return cls_method is not obj_method + class AutoCreator(BaseCreator): """Creator which is automatically triggered without user interaction. @@ -731,6 +754,11 @@ class AutoCreator(BaseCreator): """Skip removement.""" pass + def _method_is_overriden(self, method_name): + cls_method = getattr(AutoCreator, method_name) + obj_method = getattr(self.__class__, method_name) + return cls_method is not obj_method + def discover_creator_plugins(*args, **kwargs): return discover(BaseCreator, *args, **kwargs) From c3847aec5186d26414f220369a89234f70740706 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 5 Sep 2023 17:55:37 +0200 Subject: [PATCH 301/327] removed '_method_is_overriden' and use explicit class checks --- openpype/pipeline/create/creator_plugins.py | 40 ++++++--------------- 1 file changed, 10 insertions(+), 30 deletions(-) diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index ab0d36e67e..6aa08cae70 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -214,8 +214,16 @@ class BaseCreator: # Backwards compatibility for system settings self.apply_settings(project_settings, system_settings) - init_overriden = self._method_is_overriden("__init__") - if init_overriden or expect_system_settings: + init_use_base = any( + self.__class__.__init__ is cls.__init__ + for cls in { + BaseCreator, + Creator, + HiddenCreator, + AutoCreator, + } + ) + if not init_use_base or expect_system_settings: self.log.warning(( "WARNING: Source - Create plugin {}." " System settings argument will not be passed to" @@ -225,19 +233,6 @@ class BaseCreator: " need to keep system settings." ).format(self.__class__.__name__)) - def _method_is_overriden(self, method_name): - """Check if method is overriden on objects class. - - Implemented for deprecation warning validation on init. - - Returns: - bool: True if method is overriden on objects class. - """ - - cls_method = getattr(BaseCreator, method_name) - obj_method = getattr(self.__class__, method_name) - return cls_method is not obj_method - def apply_settings(self, project_settings): """Method called on initialization of plugin to apply settings. @@ -591,11 +586,6 @@ class Creator(BaseCreator): ) super(Creator, self).__init__(*args, **kwargs) - def _method_is_overriden(self, method_name): - cls_method = getattr(Creator, method_name) - obj_method = getattr(self.__class__, method_name) - return cls_method is not obj_method - @property def show_order(self): """Order in which is creator shown in UI. @@ -738,11 +728,6 @@ class HiddenCreator(BaseCreator): def create(self, instance_data, source_data): pass - def _method_is_overriden(self, method_name): - cls_method = getattr(HiddenCreator, method_name) - obj_method = getattr(self.__class__, method_name) - return cls_method is not obj_method - class AutoCreator(BaseCreator): """Creator which is automatically triggered without user interaction. @@ -754,11 +739,6 @@ class AutoCreator(BaseCreator): """Skip removement.""" pass - def _method_is_overriden(self, method_name): - cls_method = getattr(AutoCreator, method_name) - obj_method = getattr(self.__class__, method_name) - return cls_method is not obj_method - def discover_creator_plugins(*args, **kwargs): return discover(BaseCreator, *args, **kwargs) From 1b7e1aeed7c09014855f5d16bef37f22f917f3f0 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 5 Sep 2023 18:12:49 +0200 Subject: [PATCH 302/327] Fix attribute definitions for `CreateYetiCache` --- openpype/hosts/maya/plugins/create/create_yeti_cache.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_yeti_cache.py b/openpype/hosts/maya/plugins/create/create_yeti_cache.py index 395aa62325..ca002392d4 100644 --- a/openpype/hosts/maya/plugins/create/create_yeti_cache.py +++ b/openpype/hosts/maya/plugins/create/create_yeti_cache.py @@ -13,8 +13,7 @@ class CreateYetiCache(plugin.MayaCreator): family = "yeticache" icon = "pagelines" - def __init__(self, *args, **kwargs): - super(CreateYetiCache, self).__init__(*args, **kwargs) + def get_instance_attr_defs(self): defs = [ NumberDef("preroll", @@ -36,3 +35,5 @@ class CreateYetiCache(plugin.MayaCreator): default=3, decimals=0) ) + + return defs From 896b80322cec4158124a8e43c5c5036059393c0b Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 6 Sep 2023 03:25:11 +0000 Subject: [PATCH 303/327] [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 7de6fd752b..466f9ce033 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.16.5-nightly.4" +__version__ = "3.16.5-nightly.5" From 83f4c2ea39012f3dc15890dddf421e86606a4dd4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 6 Sep 2023 03:25:50 +0000 Subject: [PATCH 304/327] 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 83a4b6a8d4..f6d1a25dc2 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.16.5-nightly.5 - 3.16.5-nightly.4 - 3.16.5-nightly.3 - 3.16.5-nightly.2 @@ -134,7 +135,6 @@ body: - 3.14.9-nightly.2 - 3.14.9-nightly.1 - 3.14.8 - - 3.14.8-nightly.4 validations: required: true - type: dropdown From caad3e57e67f3f54854c33dd19c6d482fe977f54 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 6 Sep 2023 08:00:16 +0000 Subject: [PATCH 305/327] [Automated] Release --- CHANGELOG.md | 673 ++++++++++++++++++++++++++++++++++++++++++++ openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 675 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1948b1a3f..c4f9ff57ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,679 @@ # Changelog +## [3.16.5](https://github.com/ynput/OpenPype/tree/3.16.5) + + +[Full Changelog](https://github.com/ynput/OpenPype/compare/3.16.4...3.16.5) + +### **🆕 New features** + + +
+Attribute Definitions: Multiselection enum def #5547 + +Added `multiselection` option to `EnumDef`. + + +___ + +
+ +### **🚀 Enhancements** + + +
+Farm: adding target collector #5494 + +Enhancing farm publishing workflow. + + +___ + +
+ + +
+Maya: Optimize validate plug-in path attributes #5522 + +- Optimize query (use `cmds.ls` once) +- Add Select Invalid action +- Improve validation report +- Avoid "Unknown object type" errors + + +___ + +
+ + +
+Maya: Remove Validate Instance Attributes plug-in #5525 + +Remove Validate Instance Attributes plug-in. + + +___ + +
+ + +
+Enhancement: Tweak logging for artist facing reports #5537 + +Tweak the logging of publishing for global, deadline, maya and a fusion plugin to have a cleaner artist-facing report. +- Fix context being reported correctly from CollectContext +- Fix ValidateMeshArnoldAttributes: fix when arnold is not loaded, fix applying settings, fix for when ai attributes do not exist + + +___ + +
+ + +
+AYON: Update settings #5544 + +Updated settings in AYON addons and conversion of AYON settings in OpenPype. + + +___ + +
+ + +
+Chore: Removed Ass export script #5560 + +Removed Arnold render script, which was obsolete and unused. + + +___ + +
+ + +
+Nuke: Allow for knob values to be validated against multiple values. #5042 + +Knob values can now be validated against multiple values, so you can allow write nodes to be `exr` and `png`, or `16-bit` and `32-bit`. + + +___ + +
+ + +
+Enhancement: Cosmetics for Higher version of publish already exists validation error #5190 + +Fix double spaces in message.Example output **after** the PR: + + +___ + +
+ + +
+Nuke: publish existing frames on farm #5409 + +This PR proposes adding a fourth option in Nuke render publish called "Use Existing Frames - Farm". This would be useful when the farm is busy or when the artist lacks enough farm licenses. Additionally, some artists prefer rendering on the farm but still want to check frames before publishing.By adding the "Use Existing Frames - Farm" option, artists will have more flexibility and control over their render publishing process. This enhancement will streamline the workflow and improve efficiency for Nuke users. + + +___ + +
+ + +
+Unreal: Create project in temp location and move to final when done #5476 + +Create Unreal project in local temporary folder and when done, move it to final destination. + + +___ + +
+ + +
+TrayPublisher: adding audio product type into default presets #5489 + +Adding Audio product type into default presets so anybody can publish audio to their shots. + + +___ + +
+ + +
+Global: avoiding cleanup of flagged representation #5502 + +Publishing folder can be flagged as persistent at representation level. + + +___ + +
+ + +
+General: missing tag could raise error #5511 + +- avoiding potential situation where missing Tag key could raise error + + +___ + +
+ + +
+Chore: Queued event system #5514 + +Implemented event system with more expected behavior of event system. If an event is triggered during other event callback, it is not processed immediately but waits until all callbacks of previous events are done. The event system also allows to not trigger events directly once `emit_event` is called which gives option to process events in custom loops. + + +___ + +
+ + +
+Publisher: Tweak log message to provide plugin name after "Plugin" #5521 + +Fix logged message for settings automatically applied to plugin attributes + + +___ + +
+ + +
+Houdini: Improve VDB Selection #5523 + +Improves VDB selection if selection is `SopNode`: return the selected sop nodeif selection is `ObjNode`: get the output node with the minimum 'outputidx' or the node with display flag + + +___ + +
+ + +
+Maya: Refactor/tweak Validate Instance In same Context plug-in #5526 + +- Chore/Refactor: Re-use existing select invalid and repair actions +- Enhancement: provide more elaborate PublishValidationError report +- Bugfix: fix "optional" support by using `OptionalPyblishPluginMixin` base class. + + +___ + +
+ + +
+Enhancement: Update houdini main menu #5527 + +This PR adds two updates: +- dynamic main menu +- dynamic asset name and task + + +___ + +
+ + +
+Houdini: Reset FPS when clicking Set Frame Range #5528 + +_Similar to Maya,_ Make `Set Frame Range` resets FPS, issue https://github.com/ynput/OpenPype/issues/5516 + + +___ + +
+ + +
+Enhancement: Deadline plugins optimize, cleanup and fix optional support for validate deadline pools #5531 + +- Fix optional support of validate deadline pools +- Query deadline webservice only once per URL for verification, and once for available deadline pools instead of for every instance +- Use `deadlineUrl` in `instance.data` when validating pools if it is set. +- Code cleanup: Re-use existing `requests_get` implementation + + +___ + +
+ + +
+Chore: PowerShell script for docker build #5535 + +Added PowerShell script to run docker build. + + +___ + +
+ + +
+AYON: Deadline expand userpaths in executables list #5540 + +Expande `~` paths in executables list. + + +___ + +
+ + +
+Chore: Use correct git url #5542 + +Fixed github url in README.md. + + +___ + +
+ + +
+Chore: Create plugin does not expect system settings #5553 + +System settings are not passed to initialization of create plugin initialization (and `apply_settings`). + + +___ + +
+ + +
+Chore: Allow custom Qt scale factor rounding policy #5555 + +Do not force `PassThrough` rounding policy if different policy is defined via env variable. + + +___ + +
+ + +
+Houdini: Fix outdated containers pop-up on opening last workfile on launch #5567 + +Fix Houdini not showing outdated containers pop-up on scene open when launching with last workfile argument + + +___ + +
+ + +
+Houdini: Improve errors e.g. raise PublishValidationError or cosmetics #5568 + +Improve errors e.g. raise PublishValidationError or cosmeticsThis also fixes the Increment Current File plug-in since due to an invalid import it was previously broken + + +___ + +
+ + +
+Fusion: Code updates #5569 + +Update fusion code which contains obsolete code. Removed `switch_ui.py` script from fusion with related script in scripts. + + +___ + +
+ +### **🐛 Bug fixes** + + +
+Maya: Validate Shape Zero fix repair action + provide informational artist-facing report #5524 + +Refactor to PublishValidationError to allow the RepairAction to work + provide informational report message + + +___ + +
+ + +
+Maya: Fix attribute definitions for `CreateYetiCache` #5574 + +Fix attribute definitions for `CreateYetiCache` + + +___ + +
+ + +
+Max: Optional Renderable Camera Validator for Render Instance #5286 + +Optional validation to check on renderable camera being set up correctly for deadline submission.If not being set up correctly, it wont pass the validation and user can perform repair actions. + + +___ + +
+ + +
+Max: Adding custom modifiers back to the loaded objects #5378 + +The custom parameters OpenpypeData doesn't show in the loaded container when it is being loaded through the loader. + + +___ + +
+ + +
+Houdini: Use default_variant to Houdini Node TAB Creator #5421 + +Use the default variant of the creator plugins on the interactive creator from the TAB node search instead of hard-coding it to `Main`. + + +___ + +
+ + +
+Nuke: adding inherited colorspace from instance #5454 + +Thumbnails are extracted with inherited colorspace collected from rendering write node. + + +___ + +
+ + +
+Add kitsu credentials to deadline publish job #5455 + +This PR hopefully fixes this issue #5440 + + +___ + +
+ + +
+AYON: Fill entities during editorial #5475 + +Fill entities and update template data on instances during extract AYON hierarchy. + + +___ + +
+ + +
+Ftrack: Fix version 0 when integrating to Ftrack - OP-6595 #5477 + +Fix publishing version 0 to Ftrack. + + +___ + +
+ + +
+OCIO: windows unc path support in Nuke and Hiero #5479 + +Hiero and Nuke is not supporting windows unc path formatting in OCIO environment variable. + + +___ + +
+ + +
+Deadline: Added super call to init #5480 + +DL 10.3 requires plugin inheriting from DeadlinePlugin to call super's **init** explicitly. + + +___ + +
+ + +
+Nuke: fixing thumbnail and monitor out root attributes #5483 + +Nuke Root Colorspace settings for Thumbnail and Monitor Out schema was gradually changed between version 12, 13, 14 and we needed to address those changes individually for particular version. + + +___ + +
+ + +
+Nuke: fixing missing `instance_id` error #5484 + +Workfiles with Instances created in old publisher workflow were rising error during converting method since they were missing `instance_id` key introduced in new publisher workflow. + + +___ + +
+ + +
+Nuke: existing frames validator is repairing render target #5486 + +Nuke is now correctly repairing render target after the existing frames validator finds missing frames and repair action is used. + + +___ + +
+ + +
+added UE to extract burnins families #5487 + +This PR fixes missing burnins in reviewables when rendering from UE. +___ + +
+ + +
+Harmony: refresh code for current Deadline #5493 + +- Added support in Deadline Plug-in for new versions of Harmony, in particular version 21 and 22. +- Remove review=False flag on render instance +- Add farm=True flag on render instance +- Fix is_in_tests function call in Harmony Deadline submission plugin +- Force HarmonyOpenPype.py Deadline Python plug-in to py3 +- Fix cosmetics/hound in HarmonyOpenPype.py Deadline Python plug-in + + +___ + +
+ + +
+Publisher: Fix multiselection value #5505 + +Selection of multiple instances in Publisher does not cause that all instances change all publish attributes to the same value. + + +___ + +
+ + +
+Publisher: Avoid warnings on thumbnails if source image also has alpha channel #5510 + +Avoids the following warning from `ExtractThumbnailFromSource`: +``` +// pyblish.ExtractThumbnailFromSource : oiiotool WARNING: -o : Can't save 4 channels to jpeg... saving only R,G,B +``` + + + +___ + +
+ + +
+Update ayon-python-api #5512 + +Update ayon python api and related callbacks. + + +___ + +
+ + +
+Max: Fixing the bug of falling back to use workfile for Arnold or any renderers except Redshift #5520 + +Fix the bug of falling back to use workfile for Arnold + + +___ + +
+ + +
+General: Fix Validate Publish Dir Validator #5534 + +Nonsensical "family" key was used instead of real value (as 'render' etc.) which would result in wrong translation of intermediate family names.Updated docstring. + + +___ + +
+ + +
+have the addons loading respect a custom AYON_ADDONS_DIR #5539 + +When using a custom AYON_ADDONS_DIR environment variable that variable is used in the launcher correctly and downloads and extracts addons to there, however when running Ayon does not respect this environment variable + + +___ + +
+ + +
+Deadline: files on representation cannot be single item list #5545 + +Further logic expects that single item files will be only 'string' not 'list' (eg. repre["files"] = "abc.exr" not repre["files"] = ["abc.exr"].This would cause an issue in ExtractReview later.This could happen if DL rendered single frame file with different frame value. + + +___ + +
+ + +
+Webpublisher: better encode list values for click #5546 + +Targets could be a list, original implementation pushed it as a separate items, it must be added as `--targets webpulish --targets filepublish`.`wepublish_routes` handles triggering from UI, changes in `publish_functions` handle triggering from cmd (for tests, api access). + + +___ + +
+ + +
+Houdini: Introduce imprint function for correct version in hda loader #5548 + +Resolve #5478 + + +___ + +
+ + +
+AYON: Fill entities during editorial (2) #5549 + +Fix changes made in https://github.com/ynput/OpenPype/pull/5475. + + +___ + +
+ + +
+Max: OP Data updates in Loaders #5563 + +Fix the bug on the loaders not being able to load the objects when iterating key and values with the dict.Max prefers list over the list in dict. + + +___ + +
+ + +
+Create Plugins: Better check of overriden '__init__' method #5571 + +Create plugins do not log warning messages about each create plugin because of wrong `__init__` method check. + + +___ + +
+ +### **Merged pull requests** + + +
+Tests: fix unit tests #5533 + +Fixed failing tests.Updated Unreal's validator to match removed general one which had a couple of issues fixed. + + +___ + +
+ + + + ## [3.16.4](https://github.com/ynput/OpenPype/tree/3.16.4) diff --git a/openpype/version.py b/openpype/version.py index 466f9ce033..d5d46bab0c 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.16.5-nightly.5" +__version__ = "3.16.5" diff --git a/pyproject.toml b/pyproject.toml index a07c547123..68fbf19c91 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.16.4" # OpenPype +version = "3.16.5" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 1c667f91f90dbd3f34df971ece4453efc57ff0a4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 6 Sep 2023 08:01:23 +0000 Subject: [PATCH 306/327] 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 f6d1a25dc2..a35dbf1a17 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.16.5 - 3.16.5-nightly.5 - 3.16.5-nightly.4 - 3.16.5-nightly.3 @@ -134,7 +135,6 @@ body: - 3.14.9-nightly.3 - 3.14.9-nightly.2 - 3.14.9-nightly.1 - - 3.14.8 validations: required: true - type: dropdown From 9f8cd773bbc07e07db0536430735f97ae668fcf7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 6 Sep 2023 10:13:22 +0200 Subject: [PATCH 307/327] use start value instead of current value --- openpype/tools/publisher/widgets/screenshot_widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/publisher/widgets/screenshot_widget.py b/openpype/tools/publisher/widgets/screenshot_widget.py index 4ccf920571..170345f170 100644 --- a/openpype/tools/publisher/widgets/screenshot_widget.py +++ b/openpype/tools/publisher/widgets/screenshot_widget.py @@ -46,7 +46,7 @@ class ScreenMarquee(QtWidgets.QDialog): for screen in QtWidgets.QApplication.screens(): screen.geometryChanged.connect(self._fit_screen_geometry) - self._opacity = fade_anim.currentValue() + self._opacity = fade_anim.startValue() self._click_pos = None self._capture_rect = None From 2315ef84f1325a157f55bf85797d3e9719373392 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 6 Sep 2023 10:13:35 +0200 Subject: [PATCH 308/327] do not start animation on init --- openpype/tools/publisher/widgets/screenshot_widget.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/tools/publisher/widgets/screenshot_widget.py b/openpype/tools/publisher/widgets/screenshot_widget.py index 170345f170..64cccece6c 100644 --- a/openpype/tools/publisher/widgets/screenshot_widget.py +++ b/openpype/tools/publisher/widgets/screenshot_widget.py @@ -31,7 +31,6 @@ class ScreenMarquee(QtWidgets.QDialog): fade_anim.setEndValue(50) fade_anim.setDuration(200) fade_anim.setEasingCurve(QtCore.QEasingCurve.OutCubic) - fade_anim.start(QtCore.QAbstractAnimation.DeleteWhenStopped) fade_anim.valueChanged.connect(self._on_fade_anim) From 135cb285120495c250a8cbbace25a8581445fb1e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 6 Sep 2023 22:17:21 +0200 Subject: [PATCH 309/327] Add deprecation warning to usage of `fname` on Loader plugins --- openpype/pipeline/load/plugins.py | 13 +++++++++++++ openpype/pipeline/load/utils.py | 3 ++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/load/plugins.py b/openpype/pipeline/load/plugins.py index f87fb3312d..8acfcfdb6c 100644 --- a/openpype/pipeline/load/plugins.py +++ b/openpype/pipeline/load/plugins.py @@ -234,6 +234,19 @@ class LoaderPlugin(list): """ return cls.options or [] + @property + def fname(self): + """Backwards compatibility with deprecation warning""" + + self.log.warning(( + "DEPRECATION WARNING: Source - Loader plugin {}." + " The 'fname' property on the Loader plugin will be removed in" + " future versions of OpenPype. Planned version to drop the support" + " is 3.16.6 or 3.17.0." + ).format(self.__class__.__name__)) + if hasattr(self, "_fname"): + return self._fname + class SubsetLoaderPlugin(LoaderPlugin): """Load subset into host application diff --git a/openpype/pipeline/load/utils.py b/openpype/pipeline/load/utils.py index 42418be40e..b10d6032b3 100644 --- a/openpype/pipeline/load/utils.py +++ b/openpype/pipeline/load/utils.py @@ -318,7 +318,8 @@ def load_with_repre_context( # Backwards compatibility: Originally the loader's __init__ required the # representation context to set `fname` attribute to the filename to load - loader.fname = get_representation_path_from_context(repre_context) + # Deprecated - to be removed in OpenPype 3.16.6 or 3.17.0. + loader._fname = get_representation_path_from_context(repre_context) return loader.load(repre_context, name, namespace, options) From f954c877023f902a7ab28d2d86401829883734c0 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 6 Sep 2023 22:17:56 +0200 Subject: [PATCH 310/327] Refactor usage of deprecated `self.fname` to new style --- openpype/hosts/blender/plugins/load/load_blend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/load/load_blend.py b/openpype/hosts/blender/plugins/load/load_blend.py index 99f291a5a7..fa41f4374b 100644 --- a/openpype/hosts/blender/plugins/load/load_blend.py +++ b/openpype/hosts/blender/plugins/load/load_blend.py @@ -119,7 +119,7 @@ class BlendLoader(plugin.AssetLoader): context: Full parenthood of representation to load options: Additional settings dictionary """ - libpath = self.fname + libpath = self.filepath_from_context(context) asset = context["asset"]["name"] subset = context["subset"]["name"] From ba6dfc5eadd1a4d48f92924a2e1bcf3c7d5524f6 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 6 Sep 2023 23:28:39 +0200 Subject: [PATCH 311/327] Remove unused variables + tweak logs --- .../hosts/blender/plugins/load/load_camera_abc.py | 2 +- .../hosts/blender/plugins/load/load_camera_fbx.py | 2 +- .../hosts/blender/plugins/publish/extract_abc.py | 2 -- .../plugins/publish/extract_abc_animation.py | 2 -- .../blender/plugins/publish/extract_camera_abc.py | 5 ----- openpype/hosts/flame/plugins/load/load_clip.py | 1 - .../hosts/flame/plugins/load/load_clip_batch.py | 1 - .../plugins/publish/collect_timeline_instances.py | 1 - .../hosts/harmony/plugins/load/load_template.py | 1 - openpype/hosts/hiero/api/plugin.py | 14 -------------- .../hiero/plugins/publish/collect_clip_effects.py | 2 -- openpype/hosts/houdini/plugins/load/load_bgeo.py | 1 - openpype/hosts/max/plugins/create/create_render.py | 1 - .../hosts/max/plugins/publish/collect_render.py | 1 - .../max/plugins/publish/extract_camera_abc.py | 2 -- .../max/plugins/publish/extract_camera_fbx.py | 3 +-- .../max/plugins/publish/extract_max_scene_raw.py | 3 +-- .../hosts/max/plugins/publish/extract_model.py | 4 +--- .../hosts/max/plugins/publish/extract_model_fbx.py | 5 +---- .../hosts/max/plugins/publish/extract_model_obj.py | 4 +--- .../max/plugins/publish/extract_pointcache.py | 2 -- .../max/plugins/publish/extract_redshift_proxy.py | 3 +-- openpype/hosts/maya/api/plugin.py | 1 - .../maya/plugins/inventory/import_reference.py | 1 - .../hosts/maya/plugins/load/load_multiverse_usd.py | 2 -- openpype/hosts/maya/plugins/load/load_reference.py | 4 ++-- openpype/hosts/maya/plugins/load/load_xgen.py | 2 -- .../plugins/publish/collect_multiverse_look.py | 1 - .../hosts/maya/tools/mayalookassigner/widgets.py | 4 +--- .../hosts/nuke/plugins/load/load_camera_abc.py | 2 -- .../plugins/publish/extract_review_data_lut.py | 1 - .../nuke/plugins/publish/extract_thumbnail.py | 2 -- .../plugins/publish/collect_auto_image.py | 1 - openpype/hosts/resolve/api/plugin.py | 10 ---------- .../plugins/publish/extract_thumbnail.py | 2 -- .../tvpaint/plugins/load/load_reference_image.py | 2 +- .../tvpaint/plugins/publish/extract_sequence.py | 3 --- .../hosts/unreal/plugins/publish/extract_uasset.py | 3 +-- 38 files changed, 13 insertions(+), 90 deletions(-) diff --git a/openpype/hosts/blender/plugins/load/load_camera_abc.py b/openpype/hosts/blender/plugins/load/load_camera_abc.py index e5afecff66..05d3fb764d 100644 --- a/openpype/hosts/blender/plugins/load/load_camera_abc.py +++ b/openpype/hosts/blender/plugins/load/load_camera_abc.py @@ -100,7 +100,7 @@ class AbcCameraLoader(plugin.AssetLoader): asset_group = bpy.data.objects.new(group_name, object_data=None) avalon_container.objects.link(asset_group) - objects = self._process(libpath, asset_group, group_name) + self._process(libpath, asset_group, group_name) objects = [] nodes = list(asset_group.children) diff --git a/openpype/hosts/blender/plugins/load/load_camera_fbx.py b/openpype/hosts/blender/plugins/load/load_camera_fbx.py index b9d05dda0a..3cca6e7fd3 100644 --- a/openpype/hosts/blender/plugins/load/load_camera_fbx.py +++ b/openpype/hosts/blender/plugins/load/load_camera_fbx.py @@ -103,7 +103,7 @@ class FbxCameraLoader(plugin.AssetLoader): asset_group = bpy.data.objects.new(group_name, object_data=None) avalon_container.objects.link(asset_group) - objects = self._process(libpath, asset_group, group_name) + self._process(libpath, asset_group, group_name) objects = [] nodes = list(asset_group.children) diff --git a/openpype/hosts/blender/plugins/publish/extract_abc.py b/openpype/hosts/blender/plugins/publish/extract_abc.py index f4babc94d3..87159e53f0 100644 --- a/openpype/hosts/blender/plugins/publish/extract_abc.py +++ b/openpype/hosts/blender/plugins/publish/extract_abc.py @@ -21,8 +21,6 @@ class ExtractABC(publish.Extractor): filename = f"{instance.name}.abc" filepath = os.path.join(stagingdir, filename) - context = bpy.context - # Perform extraction self.log.info("Performing extraction..") diff --git a/openpype/hosts/blender/plugins/publish/extract_abc_animation.py b/openpype/hosts/blender/plugins/publish/extract_abc_animation.py index e141ccaa44..44b2ba3761 100644 --- a/openpype/hosts/blender/plugins/publish/extract_abc_animation.py +++ b/openpype/hosts/blender/plugins/publish/extract_abc_animation.py @@ -20,8 +20,6 @@ class ExtractAnimationABC(publish.Extractor): filename = f"{instance.name}.abc" filepath = os.path.join(stagingdir, filename) - context = bpy.context - # Perform extraction self.log.info("Performing extraction..") diff --git a/openpype/hosts/blender/plugins/publish/extract_camera_abc.py b/openpype/hosts/blender/plugins/publish/extract_camera_abc.py index a21a59b151..036be7bf3c 100644 --- a/openpype/hosts/blender/plugins/publish/extract_camera_abc.py +++ b/openpype/hosts/blender/plugins/publish/extract_camera_abc.py @@ -21,16 +21,11 @@ class ExtractCameraABC(publish.Extractor): filename = f"{instance.name}.abc" filepath = os.path.join(stagingdir, filename) - context = bpy.context - # Perform extraction self.log.info("Performing extraction..") plugin.deselect_all() - selected = [] - active = None - asset_group = None for obj in instance: if obj.get(AVALON_PROPERTY): diff --git a/openpype/hosts/flame/plugins/load/load_clip.py b/openpype/hosts/flame/plugins/load/load_clip.py index 338833b449..ca4eab0f63 100644 --- a/openpype/hosts/flame/plugins/load/load_clip.py +++ b/openpype/hosts/flame/plugins/load/load_clip.py @@ -48,7 +48,6 @@ class LoadClip(opfapi.ClipLoader): self.fpd = fproject.current_workspace.desktop # load clip to timeline and get main variables - namespace = namespace version = context['version'] version_data = version.get("data", {}) version_name = version.get("name", None) diff --git a/openpype/hosts/flame/plugins/load/load_clip_batch.py b/openpype/hosts/flame/plugins/load/load_clip_batch.py index ca43b94ee9..1f3a017d72 100644 --- a/openpype/hosts/flame/plugins/load/load_clip_batch.py +++ b/openpype/hosts/flame/plugins/load/load_clip_batch.py @@ -45,7 +45,6 @@ class LoadClipBatch(opfapi.ClipLoader): self.batch = options.get("batch") or flame.batch # load clip to timeline and get main variables - namespace = namespace version = context['version'] version_data = version.get("data", {}) version_name = version.get("name", None) diff --git a/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py b/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py index 23fdf5e785..e14f960a2b 100644 --- a/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py +++ b/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py @@ -325,7 +325,6 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin): def _create_shot_instance(self, context, clip_name, **data): master_layer = data.get("heroTrack") hierarchy_data = data.get("hierarchyData") - asset = data.get("asset") if not master_layer: return diff --git a/openpype/hosts/harmony/plugins/load/load_template.py b/openpype/hosts/harmony/plugins/load/load_template.py index f3c69a9104..a78a1bf1ec 100644 --- a/openpype/hosts/harmony/plugins/load/load_template.py +++ b/openpype/hosts/harmony/plugins/load/load_template.py @@ -82,7 +82,6 @@ class TemplateLoader(load.LoaderPlugin): node = harmony.find_node_by_name(node_name, "GROUP") self_name = self.__class__.__name__ - update_and_replace = False if is_representation_from_latest(representation): self._set_green(node) else: diff --git a/openpype/hosts/hiero/api/plugin.py b/openpype/hosts/hiero/api/plugin.py index 65a4009756..52f96261b2 100644 --- a/openpype/hosts/hiero/api/plugin.py +++ b/openpype/hosts/hiero/api/plugin.py @@ -317,20 +317,6 @@ class Spacer(QtWidgets.QWidget): self.setLayout(layout) -def get_reference_node_parents(ref): - """Return all parent reference nodes of reference node - - Args: - ref (str): reference node. - - Returns: - list: The upstream parent reference nodes. - - """ - parents = [] - return parents - - class SequenceLoader(LoaderPlugin): """A basic SequenceLoader for Resolve diff --git a/openpype/hosts/hiero/plugins/publish/collect_clip_effects.py b/openpype/hosts/hiero/plugins/publish/collect_clip_effects.py index d455ad4a4e..fcb1ab27a0 100644 --- a/openpype/hosts/hiero/plugins/publish/collect_clip_effects.py +++ b/openpype/hosts/hiero/plugins/publish/collect_clip_effects.py @@ -43,7 +43,6 @@ class CollectClipEffects(pyblish.api.InstancePlugin): if review and review_track_index == _track_index: continue for sitem in sub_track_items: - effect = None # make sure this subtrack item is relative of track item if ((track_item not in sitem.linkedItems()) and (len(sitem.linkedItems()) > 0)): @@ -53,7 +52,6 @@ class CollectClipEffects(pyblish.api.InstancePlugin): continue effect = self.add_effect(_track_index, sitem) - if effect: effects.update(effect) diff --git a/openpype/hosts/houdini/plugins/load/load_bgeo.py b/openpype/hosts/houdini/plugins/load/load_bgeo.py index 22680178c0..489bf944ed 100644 --- a/openpype/hosts/houdini/plugins/load/load_bgeo.py +++ b/openpype/hosts/houdini/plugins/load/load_bgeo.py @@ -34,7 +34,6 @@ class BgeoLoader(load.LoaderPlugin): # Create a new geo node container = obj.createNode("geo", node_name=node_name) - is_sequence = bool(context["representation"]["context"].get("frame")) # Remove the file node, it only loads static meshes # Houdini 17 has removed the file node from the geo node diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index 235046684e..9cc3c8da8a 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -14,7 +14,6 @@ class CreateRender(plugin.MaxCreator): def create(self, subset_name, instance_data, pre_create_data): from pymxs import runtime as rt - sel_obj = list(rt.selection) file = rt.maxFileName filename, _ = os.path.splitext(file) instance_data["AssetName"] = filename diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 8ee2f43103..2dfa1520a9 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -30,7 +30,6 @@ class CollectRender(pyblish.api.InstancePlugin): asset = get_current_asset_name() files_by_aov = RenderProducts().get_beauty(instance.name) - folder = folder.replace("\\", "/") aovs = RenderProducts().get_aovs(instance.name) files_by_aov.update(aovs) diff --git a/openpype/hosts/max/plugins/publish/extract_camera_abc.py b/openpype/hosts/max/plugins/publish/extract_camera_abc.py index b42732e70d..b1918c53e0 100644 --- a/openpype/hosts/max/plugins/publish/extract_camera_abc.py +++ b/openpype/hosts/max/plugins/publish/extract_camera_abc.py @@ -22,8 +22,6 @@ class ExtractCameraAlembic(publish.Extractor, OptionalPyblishPluginMixin): start = float(instance.data.get("frameStartHandle", 1)) end = float(instance.data.get("frameEndHandle", 1)) - container = instance.data["instance_node"] - self.log.info("Extracting Camera ...") stagingdir = self.staging_dir(instance) diff --git a/openpype/hosts/max/plugins/publish/extract_camera_fbx.py b/openpype/hosts/max/plugins/publish/extract_camera_fbx.py index 06ac3da093..537c88eb4d 100644 --- a/openpype/hosts/max/plugins/publish/extract_camera_fbx.py +++ b/openpype/hosts/max/plugins/publish/extract_camera_fbx.py @@ -19,9 +19,8 @@ class ExtractCameraFbx(publish.Extractor, OptionalPyblishPluginMixin): def process(self, instance): if not self.is_active(instance.data): return - container = instance.data["instance_node"] - self.log.info("Extracting Camera ...") + self.log.debug("Extracting Camera ...") stagingdir = self.staging_dir(instance) filename = "{name}.fbx".format(**instance.data) diff --git a/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py b/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py index de5db9ab56..a7a889c587 100644 --- a/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py +++ b/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py @@ -18,10 +18,9 @@ class ExtractMaxSceneRaw(publish.Extractor, OptionalPyblishPluginMixin): def process(self, instance): if not self.is_active(instance.data): return - container = instance.data["instance_node"] # publish the raw scene for camera - self.log.info("Extracting Raw Max Scene ...") + self.log.debug("Extracting Raw Max Scene ...") stagingdir = self.staging_dir(instance) filename = "{name}.max".format(**instance.data) diff --git a/openpype/hosts/max/plugins/publish/extract_model.py b/openpype/hosts/max/plugins/publish/extract_model.py index c7ecf7efc9..38f4848c5e 100644 --- a/openpype/hosts/max/plugins/publish/extract_model.py +++ b/openpype/hosts/max/plugins/publish/extract_model.py @@ -20,9 +20,7 @@ class ExtractModel(publish.Extractor, OptionalPyblishPluginMixin): if not self.is_active(instance.data): return - container = instance.data["instance_node"] - - self.log.info("Extracting Geometry ...") + self.log.debug("Extracting Geometry ...") stagingdir = self.staging_dir(instance) filename = "{name}.abc".format(**instance.data) diff --git a/openpype/hosts/max/plugins/publish/extract_model_fbx.py b/openpype/hosts/max/plugins/publish/extract_model_fbx.py index 56c2cadd94..fd48ed5007 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_fbx.py +++ b/openpype/hosts/max/plugins/publish/extract_model_fbx.py @@ -20,10 +20,7 @@ class ExtractModelFbx(publish.Extractor, OptionalPyblishPluginMixin): if not self.is_active(instance.data): return - container = instance.data["instance_node"] - - - self.log.info("Extracting Geometry ...") + self.log.debug("Extracting Geometry ...") stagingdir = self.staging_dir(instance) filename = "{name}.fbx".format(**instance.data) diff --git a/openpype/hosts/max/plugins/publish/extract_model_obj.py b/openpype/hosts/max/plugins/publish/extract_model_obj.py index 4fde65cf22..e522b1e7a1 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_obj.py +++ b/openpype/hosts/max/plugins/publish/extract_model_obj.py @@ -20,9 +20,7 @@ class ExtractModelObj(publish.Extractor, OptionalPyblishPluginMixin): if not self.is_active(instance.data): return - container = instance.data["instance_node"] - - self.log.info("Extracting Geometry ...") + self.log.debug("Extracting Geometry ...") stagingdir = self.staging_dir(instance) filename = "{name}.obj".format(**instance.data) diff --git a/openpype/hosts/max/plugins/publish/extract_pointcache.py b/openpype/hosts/max/plugins/publish/extract_pointcache.py index 5a99a8b845..c3de623bc0 100644 --- a/openpype/hosts/max/plugins/publish/extract_pointcache.py +++ b/openpype/hosts/max/plugins/publish/extract_pointcache.py @@ -54,8 +54,6 @@ class ExtractAlembic(publish.Extractor): start = float(instance.data.get("frameStartHandle", 1)) end = float(instance.data.get("frameEndHandle", 1)) - container = instance.data["instance_node"] - self.log.debug("Extracting pointcache ...") parent_dir = self.staging_dir(instance) diff --git a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py index ab569ecbcb..f67ed30c6b 100644 --- a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py +++ b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py @@ -16,11 +16,10 @@ class ExtractRedshiftProxy(publish.Extractor): families = ["redshiftproxy"] def process(self, instance): - container = instance.data["instance_node"] start = int(instance.context.data.get("frameStart")) end = int(instance.context.data.get("frameEnd")) - self.log.info("Extracting Redshift Proxy...") + self.log.debug("Extracting Redshift Proxy...") stagingdir = self.staging_dir(instance) rs_filename = "{name}.rs".format(**instance.data) rs_filepath = os.path.join(stagingdir, rs_filename) diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index 3f383fafb8..4032618afb 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -683,7 +683,6 @@ class ReferenceLoader(Loader): loaded_containers.append(container) self._organize_containers(nodes, container) c += 1 - namespace = None return loaded_containers diff --git a/openpype/hosts/maya/plugins/inventory/import_reference.py b/openpype/hosts/maya/plugins/inventory/import_reference.py index ecc424209d..3f3b85ba6c 100644 --- a/openpype/hosts/maya/plugins/inventory/import_reference.py +++ b/openpype/hosts/maya/plugins/inventory/import_reference.py @@ -12,7 +12,6 @@ class ImportReference(InventoryAction): color = "#d8d8d8" def process(self, containers): - references = cmds.ls(type="reference") for container in containers: if container["loader"] != "ReferenceLoader": print("Not a reference, skipping") diff --git a/openpype/hosts/maya/plugins/load/load_multiverse_usd.py b/openpype/hosts/maya/plugins/load/load_multiverse_usd.py index d08fcd904e..cad42b55f9 100644 --- a/openpype/hosts/maya/plugins/load/load_multiverse_usd.py +++ b/openpype/hosts/maya/plugins/load/load_multiverse_usd.py @@ -43,8 +43,6 @@ class MultiverseUsdLoader(load.LoaderPlugin): import multiverse # Create the shape - shape = None - transform = None with maintained_selection(): cmds.namespace(addNamespace=namespace) with namespaced(namespace, new=False): diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index 91767249e0..61f337f501 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -205,7 +205,7 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): cmds.setAttr("{}.selectHandleZ".format(group_name), cz) if family == "rig": - self._post_process_rig(name, namespace, context, options) + self._post_process_rig(namespace, context, options) else: if "translate" in options: if not attach_to_root and new_nodes: @@ -229,7 +229,7 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): members = get_container_members(container) self._lock_camera_transforms(members) - def _post_process_rig(self, name, namespace, context, options): + def _post_process_rig(self, namespace, context, options): nodes = self[:] create_rig_animation_instance( diff --git a/openpype/hosts/maya/plugins/load/load_xgen.py b/openpype/hosts/maya/plugins/load/load_xgen.py index 323f8d7eda..2ad6ad55bc 100644 --- a/openpype/hosts/maya/plugins/load/load_xgen.py +++ b/openpype/hosts/maya/plugins/load/load_xgen.py @@ -53,8 +53,6 @@ class XgenLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): ) # Reference xgen. Xgen does not like being referenced in under a group. - new_nodes = [] - with maintained_selection(): nodes = cmds.file( maya_filepath, diff --git a/openpype/hosts/maya/plugins/publish/collect_multiverse_look.py b/openpype/hosts/maya/plugins/publish/collect_multiverse_look.py index f05fb76d48..bcb979edfc 100644 --- a/openpype/hosts/maya/plugins/publish/collect_multiverse_look.py +++ b/openpype/hosts/maya/plugins/publish/collect_multiverse_look.py @@ -281,7 +281,6 @@ class CollectMultiverseLookData(pyblish.api.InstancePlugin): long=True) nodes.update(nodes_of_interest) - files = [] sets = {} instance.data["resources"] = [] publishMipMap = instance.data["publishMipMap"] diff --git a/openpype/hosts/maya/tools/mayalookassigner/widgets.py b/openpype/hosts/maya/tools/mayalookassigner/widgets.py index f2df17e68c..82c37e2104 100644 --- a/openpype/hosts/maya/tools/mayalookassigner/widgets.py +++ b/openpype/hosts/maya/tools/mayalookassigner/widgets.py @@ -90,15 +90,13 @@ class AssetOutliner(QtWidgets.QWidget): def get_all_assets(self): """Add all items from the current scene""" - items = [] with preserve_expanded_rows(self.view): with preserve_selection(self.view): self.clear() nodes = commands.get_all_asset_nodes() items = commands.create_items_from_nodes(nodes) self.add_items(items) - - return len(items) > 0 + return len(items) > 0 def get_selected_assets(self): """Add all selected items from the current scene""" diff --git a/openpype/hosts/nuke/plugins/load/load_camera_abc.py b/openpype/hosts/nuke/plugins/load/load_camera_abc.py index fec4ee556e..2939ceebae 100644 --- a/openpype/hosts/nuke/plugins/load/load_camera_abc.py +++ b/openpype/hosts/nuke/plugins/load/load_camera_abc.py @@ -112,8 +112,6 @@ class AlembicCameraLoader(load.LoaderPlugin): version_doc = get_version_by_id(project_name, representation["parent"]) object_name = container['objectName'] - # get corresponding node - camera_node = nuke.toNode(object_name) # get main variables version_data = version_doc.get("data", {}) diff --git a/openpype/hosts/nuke/plugins/publish/extract_review_data_lut.py b/openpype/hosts/nuke/plugins/publish/extract_review_data_lut.py index e4b7b155cd..2a26ed82fb 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_review_data_lut.py +++ b/openpype/hosts/nuke/plugins/publish/extract_review_data_lut.py @@ -20,7 +20,6 @@ class ExtractReviewDataLut(publish.Extractor): hosts = ["nuke"] def process(self, instance): - families = instance.data["families"] self.log.info("Creating staging dir...") if "representations" in instance.data: staging_dir = instance.data[ diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index d57d55f85d..b20df4ffe2 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -91,8 +91,6 @@ class ExtractThumbnail(publish.Extractor): if collection: # get path - fname = os.path.basename(collection.format( - "{head}{padding}{tail}")) fhead = collection.format("{head}") thumb_fname = list(collection)[mid_frame] diff --git a/openpype/hosts/photoshop/plugins/publish/collect_auto_image.py b/openpype/hosts/photoshop/plugins/publish/collect_auto_image.py index f1d8419608..77f1a3e91f 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_auto_image.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_auto_image.py @@ -16,7 +16,6 @@ class CollectAutoImage(pyblish.api.ContextPlugin): targets = ["automated"] def process(self, context): - family = "image" for instance in context: creator_identifier = instance.data.get("creator_identifier") if creator_identifier and creator_identifier == "auto_image": diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index 59c27f29da..e2bd76ffa2 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -413,8 +413,6 @@ class ClipLoader: if self.with_handles: source_in -= handle_start source_out += handle_end - handle_start = 0 - handle_end = 0 # make track item from source in bin as item timeline_item = lib.create_timeline_item( @@ -433,14 +431,6 @@ class ClipLoader: self.data["path"], self.active_bin) _clip_property = media_pool_item.GetClipProperty - # get handles - handle_start = self.data["versionData"].get("handleStart") - handle_end = self.data["versionData"].get("handleEnd") - if handle_start is None: - handle_start = int(self.data["assetData"]["handleStart"]) - if handle_end is None: - handle_end = int(self.data["assetData"]["handleEnd"]) - source_in = int(_clip_property("Start")) source_out = int(_clip_property("End")) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py index b99503b3c8..a2afd160fa 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py @@ -49,8 +49,6 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin): else: first_filename = files - staging_dir = None - # Convert to jpeg if not yet full_input_path = os.path.join( thumbnail_repre["stagingDir"], first_filename diff --git a/openpype/hosts/tvpaint/plugins/load/load_reference_image.py b/openpype/hosts/tvpaint/plugins/load/load_reference_image.py index edc116a8e4..3707ef97aa 100644 --- a/openpype/hosts/tvpaint/plugins/load/load_reference_image.py +++ b/openpype/hosts/tvpaint/plugins/load/load_reference_image.py @@ -171,7 +171,7 @@ class LoadImage(plugin.Loader): george_script = "\n".join(george_script_lines) execute_george_through_file(george_script) - def _remove_container(self, container, members=None): + def _remove_container(self, container): if not container: return representation = container["representation"] diff --git a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py index 8a610cf388..a13a91de46 100644 --- a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -63,7 +63,6 @@ class ExtractSequence(pyblish.api.Extractor): "ignoreLayersTransparency", False ) - family_lowered = instance.data["family"].lower() mark_in = instance.context.data["sceneMarkIn"] mark_out = instance.context.data["sceneMarkOut"] @@ -76,11 +75,9 @@ class ExtractSequence(pyblish.api.Extractor): # Frame start/end may be stored as float frame_start = int(instance.data["frameStart"]) - frame_end = int(instance.data["frameEnd"]) # Handles are not stored per instance but on Context handle_start = instance.context.data["handleStart"] - handle_end = instance.context.data["handleEnd"] scene_bg_color = instance.context.data["sceneBgColor"] diff --git a/openpype/hosts/unreal/plugins/publish/extract_uasset.py b/openpype/hosts/unreal/plugins/publish/extract_uasset.py index 48b62faa97..0dd7ff4a0d 100644 --- a/openpype/hosts/unreal/plugins/publish/extract_uasset.py +++ b/openpype/hosts/unreal/plugins/publish/extract_uasset.py @@ -19,9 +19,8 @@ class ExtractUAsset(publish.Extractor): "umap" if "umap" in instance.data.get("families") else "uasset") ar = unreal.AssetRegistryHelpers.get_asset_registry() - self.log.info("Performing extraction..") + self.log.debug("Performing extraction..") staging_dir = self.staging_dir(instance) - filename = f"{instance.name}.{extension}" members = instance.data.get("members", []) From e79f1ef4b9b48f085f42ac43344404580522d10c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 6 Sep 2023 23:30:17 +0200 Subject: [PATCH 312/327] Replacing value only if value is in `str` is same as just replacing it --- .../hosts/houdini/plugins/publish/collect_vray_rop.py | 9 ++------- openpype/hosts/maya/api/lib_rendersettings.py | 9 ++------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py index d4fe37f993..277f922ba4 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py @@ -80,14 +80,9 @@ class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin): def get_beauty_render_product(self, prefix, suffix=""): """Return the beauty output filename if render element enabled """ + # Remove aov suffix from the product: `prefix.aov_suffix` -> `prefix` aov_parm = ".{}".format(suffix) - beauty_product = None - if aov_parm in prefix: - beauty_product = prefix.replace(aov_parm, "") - else: - beauty_product = prefix - - return beauty_product + return prefix.replace(aov_parm, "") def get_render_element_name(self, node, prefix, suffix=""): """Return the output filename using the AOV prefix and suffix diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index f54633c04d..42cf29d0a7 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -177,12 +177,7 @@ class RenderSettings(object): # list all the aovs all_rs_aovs = cmds.ls(type='RedshiftAOV') for rs_aov in redshift_aovs: - rs_layername = rs_aov - if " " in rs_aov: - rs_renderlayer = rs_aov.replace(" ", "") - rs_layername = "rsAov_{}".format(rs_renderlayer) - else: - rs_layername = "rsAov_{}".format(rs_aov) + rs_layername = "rsAov_{}".format(rs_aov.replace(" ", "")) if rs_layername in all_rs_aovs: continue cmds.rsCreateAov(type=rs_aov) @@ -317,7 +312,7 @@ class RenderSettings(object): separators = [cmds.menuItem(i, query=True, label=True) for i in items] # noqa: E501 try: sep_idx = separators.index(aov_separator) - except ValueError as e: + except ValueError: six.reraise( CreatorError, CreatorError( From c1b305a4462b6e58eede2853a5cd0623cf4ef925 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 6 Sep 2023 23:31:33 +0200 Subject: [PATCH 313/327] Fix code (`if not found` should not have been nest into the for loop) + simplify logic --- openpype/hosts/max/api/lib_rendersettings.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/max/api/lib_rendersettings.py b/openpype/hosts/max/api/lib_rendersettings.py index afde5008d5..26e176aa8d 100644 --- a/openpype/hosts/max/api/lib_rendersettings.py +++ b/openpype/hosts/max/api/lib_rendersettings.py @@ -37,13 +37,10 @@ class RenderSettings(object): def set_render_camera(self, selection): for sel in selection: # to avoid Attribute Error from pymxs wrapper - found = False if rt.classOf(sel) in rt.Camera.classes: - found = True rt.viewport.setCamera(sel) - break - if not found: - raise RuntimeError("Active Camera not found") + return + raise RuntimeError("Active Camera not found") def render_output(self, container): folder = rt.maxFilePath From 60334621988ee749369d222d11ecd972871a2573 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 6 Sep 2023 23:32:53 +0200 Subject: [PATCH 314/327] Use asset doc and project doc from instance/context + tweak logic for values --- .../publish/validate_resolution_setting.py | 29 ++++++------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_resolution_setting.py b/openpype/hosts/max/plugins/publish/validate_resolution_setting.py index 5fcb843b20..5ac41b10a0 100644 --- a/openpype/hosts/max/plugins/publish/validate_resolution_setting.py +++ b/openpype/hosts/max/plugins/publish/validate_resolution_setting.py @@ -6,11 +6,6 @@ from openpype.pipeline import ( from pymxs import runtime as rt from openpype.hosts.max.api.lib import reset_scene_resolution -from openpype.pipeline.context_tools import ( - get_current_project_asset, - get_current_project -) - class ValidateResolutionSetting(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): @@ -43,22 +38,16 @@ class ValidateResolutionSetting(pyblish.api.InstancePlugin, "on asset or shot.") def get_db_resolution(self, instance): - data = ["data.resolutionWidth", "data.resolutionHeight"] - project_resolution = get_current_project(fields=data) - project_resolution_data = project_resolution["data"] - asset_resolution = get_current_project_asset(fields=data) - asset_resolution_data = asset_resolution["data"] - # Set project resolution - project_width = int( - project_resolution_data.get("resolutionWidth", 1920)) - project_height = int( - project_resolution_data.get("resolutionHeight", 1080)) - width = int( - asset_resolution_data.get("resolutionWidth", project_width)) - height = int( - asset_resolution_data.get("resolutionHeight", project_height)) + asset_doc = instance.data["assetEntity"] + project_doc = instance.context.data["projectEntity"] + for data in [asset_doc["data"], project_doc["data"]]: + if "resolutionWidth" in data and "resolutionHeight" in data: + width = data["resolutionWidth"] + height = data["resolutionHeight"] + return int(width), int(height) - return width, height + # Defaults if not found in asset document or project document + return 1920, 1080 @classmethod def repair(cls, instance): From 91a4ae5dfd39d64db3ded9e98b750b8d1afe22bc Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 6 Sep 2023 23:34:01 +0200 Subject: [PATCH 315/327] Remove (not yet deprecated?) `system_settings` from `apply_settings` --- .../hosts/maya/plugins/publish/extract_import_reference.py | 4 ++-- openpype/hosts/maya/plugins/publish/validate_maya_units.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_import_reference.py b/openpype/hosts/maya/plugins/publish/extract_import_reference.py index 9d2ff1a3eb..1fdee28d0c 100644 --- a/openpype/hosts/maya/plugins/publish/extract_import_reference.py +++ b/openpype/hosts/maya/plugins/publish/extract_import_reference.py @@ -30,8 +30,8 @@ class ExtractImportReference(publish.Extractor, tmp_format = "_tmp" @classmethod - def apply_settings(cls, project_setting, system_settings): - cls.active = project_setting["deadline"]["publish"]["MayaSubmitDeadline"]["import_reference"] # noqa + def apply_settings(cls, project_settings): + cls.active = project_settings["deadline"]["publish"]["MayaSubmitDeadline"]["import_reference"] # noqa def process(self, instance): if not self.is_active(instance.data): diff --git a/openpype/hosts/maya/plugins/publish/validate_maya_units.py b/openpype/hosts/maya/plugins/publish/validate_maya_units.py index 1d5619795f..ae6dc093a9 100644 --- a/openpype/hosts/maya/plugins/publish/validate_maya_units.py +++ b/openpype/hosts/maya/plugins/publish/validate_maya_units.py @@ -37,7 +37,7 @@ class ValidateMayaUnits(pyblish.api.ContextPlugin): ) @classmethod - def apply_settings(cls, project_settings, system_settings): + def apply_settings(cls, project_settings): """Apply project settings to creator""" settings = ( project_settings["maya"]["publish"]["ValidateMayaUnits"] From b601da0dbba469c1a6920622218d74ada0f589f5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 7 Sep 2023 11:40:59 +0200 Subject: [PATCH 316/327] do not fix folder in representation context --- openpype/client/server/conversion_utils.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/openpype/client/server/conversion_utils.py b/openpype/client/server/conversion_utils.py index a6c190a0fc..2dca9ad57d 100644 --- a/openpype/client/server/conversion_utils.py +++ b/openpype/client/server/conversion_utils.py @@ -663,8 +663,8 @@ def convert_v4_representation_to_v3(representation): if isinstance(context, six.string_types): context = json.loads(context) - if "folder" in context: - _c_folder = context.pop("folder") + if "asset" not in context and "folder" in context: + _c_folder = context["folder"] context["asset"] = _c_folder["name"] if "product" in context: @@ -959,9 +959,11 @@ def convert_create_representation_to_v4(representation, con): converted_representation["files"] = new_files context = representation["context"] - context["folder"] = { - "name": context.pop("asset", None) - } + if "folder" not in context: + context["folder"] = { + "name": context.get("asset") + } + context["product"] = { "type": context.pop("family", None), "name": context.pop("subset", None), @@ -1285,7 +1287,7 @@ def convert_update_representation_to_v4( if "context" in update_data: context = update_data["context"] - if "asset" in context: + if "folder" not in context and "asset" in context: context["folder"] = {"name": context.pop("asset")} if "family" in context or "subset" in context: From 7cde83933a22437d3ba4a570f3c2a77f0396e810 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 7 Sep 2023 11:50:16 +0200 Subject: [PATCH 317/327] autofix wrong representations --- openpype/client/server/conversion_utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/client/server/conversion_utils.py b/openpype/client/server/conversion_utils.py index 2dca9ad57d..f67a1ef9c4 100644 --- a/openpype/client/server/conversion_utils.py +++ b/openpype/client/server/conversion_utils.py @@ -667,6 +667,9 @@ def convert_v4_representation_to_v3(representation): _c_folder = context["folder"] context["asset"] = _c_folder["name"] + elif "asset" in context and "folder" not in context: + context["folder"] = {"name": context["asset"]} + if "product" in context: _c_product = context.pop("product") context["family"] = _c_product["type"] From 2c0a8d41006c5886d0e05e85c302f00b70c8e8f9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 7 Sep 2023 14:47:27 +0200 Subject: [PATCH 318/327] adding default factory to list objects --- .../nuke/server/settings/create_plugins.py | 4 ++++ server_addon/nuke/server/settings/imageio.py | 2 ++ .../nuke/server/settings/publish_plugins.py | 3 +++ .../nuke/server/settings/workfile_builder.py | 20 ++++++++++++++----- 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/server_addon/nuke/server/settings/create_plugins.py b/server_addon/nuke/server/settings/create_plugins.py index 0bbae4ee77..b89188a77a 100644 --- a/server_addon/nuke/server/settings/create_plugins.py +++ b/server_addon/nuke/server/settings/create_plugins.py @@ -37,6 +37,7 @@ class PrenodeModel(BaseSettingsModel): - We could not support v3 style of settings. """ knobs: list[KnobModel] = Field( + default_factory=list, title="Knobs", ) @@ -66,6 +67,7 @@ class CreateWriteRenderModel(BaseSettingsModel): (we could not support v3 style of settings) """ prenodes: list[PrenodeModel] = Field( + default_factory=list, title="Preceding nodes", ) @@ -95,6 +97,7 @@ class CreateWritePrerenderModel(BaseSettingsModel): (we could not support v3 style of settings) """ prenodes: list[PrenodeModel] = Field( + default_factory=list, title="Preceding nodes", ) @@ -124,6 +127,7 @@ class CreateWriteImageModel(BaseSettingsModel): (we could not support v3 style of settings) """ prenodes: list[PrenodeModel] = Field( + default_factory=list, title="Preceding nodes", ) diff --git a/server_addon/nuke/server/settings/imageio.py b/server_addon/nuke/server/settings/imageio.py index b43017ef8b..16f9bd309a 100644 --- a/server_addon/nuke/server/settings/imageio.py +++ b/server_addon/nuke/server/settings/imageio.py @@ -14,6 +14,7 @@ class NodesModel(BaseSettingsModel): """ _layout = "expanded" plugins: list[str] = Field( + default_factory=list, title="Used in plugins" ) # TODO: rename `nukeNodeClass` to `nuke_node_class` @@ -25,6 +26,7 @@ class NodesModel(BaseSettingsModel): in nuke integration. We could not support v3 style of settings. """ knobs: list[KnobModel] = Field( + default_factory=list, title="Knobs", ) diff --git a/server_addon/nuke/server/settings/publish_plugins.py b/server_addon/nuke/server/settings/publish_plugins.py index 7e898f8c9a..de21870d5f 100644 --- a/server_addon/nuke/server/settings/publish_plugins.py +++ b/server_addon/nuke/server/settings/publish_plugins.py @@ -46,6 +46,7 @@ class NodeModel(BaseSettingsModel): - We could not support v3 style of settings. """ knobs: list[KnobModel] = Field( + default_factory=list, title="Knobs", ) @@ -105,6 +106,7 @@ class ExtractThumbnailModel(BaseSettingsModel): """ nodes: list[NodeModel] = Field( + default_factory=list, title="Nodes (deprecated)" ) reposition_nodes: list[ThumbnailRepositionNodeModel] = Field( @@ -177,6 +179,7 @@ class ExtractReviewDataMovModel(BaseSettingsModel): enabled: bool = Field(title="Enabled") viewer_lut_raw: bool = Field(title="Viewer lut raw") outputs: list[BakingStreamModel] = Field( + default_factory=list, title="Baking streams" ) diff --git a/server_addon/nuke/server/settings/workfile_builder.py b/server_addon/nuke/server/settings/workfile_builder.py index ee67c7c16a..e9ac0db2a0 100644 --- a/server_addon/nuke/server/settings/workfile_builder.py +++ b/server_addon/nuke/server/settings/workfile_builder.py @@ -48,20 +48,30 @@ class BuilderProfileModel(BaseSettingsModel): title="Task names" ) current_context: list[BuilderProfileItemModel] = Field( - title="Current context") + default_factory=list, + title="Current context" + ) linked_assets: list[BuilderProfileItemModel] = Field( - title="Linked assets/shots") + default_factory=list, + title="Linked assets/shots" + ) class WorkfileBuilderModel(BaseSettingsModel): create_first_version: bool = Field( title="Create first workfile") custom_templates: list[CustomTemplateModel] = Field( - title="Custom templates") + default_factory=list, + title="Custom templates" + ) builder_on_start: bool = Field( - title="Run Builder at first workfile") + default=False, + title="Run Builder at first workfile" + ) profiles: list[BuilderProfileModel] = Field( - title="Builder profiles") + default_factory=list, + title="Builder profiles" + ) DEFAULT_WORKFILE_BUILDER_SETTINGS = { From ee050fe63ba6c498fd736bcf2e1bcb1226e49ed9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 7 Sep 2023 14:47:47 +0200 Subject: [PATCH 319/327] clearing todos and improving docstrings --- server_addon/nuke/server/settings/common.py | 6 ------ .../nuke/server/settings/create_plugins.py | 19 ------------------- server_addon/nuke/server/settings/dirmap.py | 13 ------------- server_addon/nuke/server/settings/imageio.py | 15 ++++----------- .../nuke/server/settings/loader_plugins.py | 8 -------- server_addon/nuke/server/settings/main.py | 4 +--- .../nuke/server/settings/publish_plugins.py | 17 ----------------- .../nuke/server/settings/scriptsmenu.py | 1 - .../settings/templated_workfile_build.py | 1 + .../nuke/server/settings/workfile_builder.py | 2 ++ 10 files changed, 8 insertions(+), 78 deletions(-) diff --git a/server_addon/nuke/server/settings/common.py b/server_addon/nuke/server/settings/common.py index 700f01f3dc..2bc3c9be81 100644 --- a/server_addon/nuke/server/settings/common.py +++ b/server_addon/nuke/server/settings/common.py @@ -89,12 +89,6 @@ knob_types_enum = [ class KnobModel(BaseSettingsModel): - """# TODO: new data structure - - v3 was having type, name, value but - ayon is not able to make it the same. Current model is - defining `type` as `text` and instead of `value` the key is `text`. - So if `type` is `boolean` then key is `boolean` (value). - """ _layout = "expanded" type: str = Field( diff --git a/server_addon/nuke/server/settings/create_plugins.py b/server_addon/nuke/server/settings/create_plugins.py index b89188a77a..80aec51ae0 100644 --- a/server_addon/nuke/server/settings/create_plugins.py +++ b/server_addon/nuke/server/settings/create_plugins.py @@ -16,13 +16,10 @@ def instance_attributes_enum(): class PrenodeModel(BaseSettingsModel): - # TODO: missing in host api - # - good for `dependency` name: str = Field( title="Node name" ) - # TODO: `nodeclass` should be renamed to `nuke_node_class` nodeclass: str = Field( "", title="Node class" @@ -32,10 +29,6 @@ class PrenodeModel(BaseSettingsModel): title="Incoming dependency" ) - """# TODO: Changes in host api: - - Need complete rework of knob types in nuke integration. - - We could not support v3 style of settings. - """ knobs: list[KnobModel] = Field( default_factory=list, title="Knobs", @@ -62,10 +55,6 @@ class CreateWriteRenderModel(BaseSettingsModel): title="Instance attributes" ) - """# TODO: Changes in host api: - - prenodes key was originally dict and now is list - (we could not support v3 style of settings) - """ prenodes: list[PrenodeModel] = Field( default_factory=list, title="Preceding nodes", @@ -92,10 +81,6 @@ class CreateWritePrerenderModel(BaseSettingsModel): title="Instance attributes" ) - """# TODO: Changes in host api: - - prenodes key was originally dict and now is list - (we could not support v3 style of settings) - """ prenodes: list[PrenodeModel] = Field( default_factory=list, title="Preceding nodes", @@ -122,10 +107,6 @@ class CreateWriteImageModel(BaseSettingsModel): title="Instance attributes" ) - """# TODO: Changes in host api: - - prenodes key was originally dict and now is list - (we could not support v3 style of settings) - """ prenodes: list[PrenodeModel] = Field( default_factory=list, title="Preceding nodes", diff --git a/server_addon/nuke/server/settings/dirmap.py b/server_addon/nuke/server/settings/dirmap.py index 2da6d7bf60..7e3c443957 100644 --- a/server_addon/nuke/server/settings/dirmap.py +++ b/server_addon/nuke/server/settings/dirmap.py @@ -25,19 +25,6 @@ class DirmapSettings(BaseSettingsModel): ) -"""# TODO: -nuke is having originally implemented -following data inputs: - -"nuke-dirmap": { - "enabled": false, - "paths": { - "source-path": [], - "destination-path": [] - } -} -""" - DEFAULT_DIRMAP_SETTINGS = { "enabled": False, "paths": { diff --git a/server_addon/nuke/server/settings/imageio.py b/server_addon/nuke/server/settings/imageio.py index 16f9bd309a..44bb72769e 100644 --- a/server_addon/nuke/server/settings/imageio.py +++ b/server_addon/nuke/server/settings/imageio.py @@ -9,22 +9,15 @@ from .common import KnobModel class NodesModel(BaseSettingsModel): - """# TODO: This needs to be somehow labeled in settings panel - or at least it could show gist of configuration - """ _layout = "expanded" plugins: list[str] = Field( default_factory=list, title="Used in plugins" ) - # TODO: rename `nukeNodeClass` to `nuke_node_class` nukeNodeClass: str = Field( title="Nuke Node Class", ) - """ # TODO: Need complete rework of knob types - in nuke integration. We could not support v3 style of settings. - """ knobs: list[KnobModel] = Field( default_factory=list, title="Knobs", @@ -172,7 +165,7 @@ class ImageIOSettings(BaseSettingsModel): _isGroup: bool = True """# TODO: enhance settings with host api: - to restruture settings for simplification. + to restructure settings for simplification. now: nuke/imageio/viewer/viewerProcess future: nuke/imageio/viewer @@ -195,7 +188,7 @@ class ImageIOSettings(BaseSettingsModel): ) """# TODO: enhance settings with host api: - to restruture settings for simplification. + to restructure settings for simplification. now: nuke/imageio/baking/viewerProcess future: nuke/imageio/baking @@ -217,9 +210,9 @@ class ImageIOSettings(BaseSettingsModel): title="Nodes" ) """# TODO: enhance settings with host api: - - old settings are using `regexInputs` key but we + - [ ] old settings are using `regexInputs` key but we need to rename to `regex_inputs` - - no need for `inputs` middle part. It can stay + - [ ] no need for `inputs` middle part. It can stay directly on `regex_inputs` """ regexInputs: RegexInputsModel = Field( diff --git a/server_addon/nuke/server/settings/loader_plugins.py b/server_addon/nuke/server/settings/loader_plugins.py index 6db381bffb..51e2c2149b 100644 --- a/server_addon/nuke/server/settings/loader_plugins.py +++ b/server_addon/nuke/server/settings/loader_plugins.py @@ -6,10 +6,6 @@ class LoadImageModel(BaseSettingsModel): enabled: bool = Field( title="Enabled" ) - """# TODO: v3 api used `_representation` - New api is hiding it so it had to be renamed - to `representations_include` - """ representations_include: list[str] = Field( default_factory=list, title="Include representations" @@ -33,10 +29,6 @@ class LoadClipModel(BaseSettingsModel): enabled: bool = Field( title="Enabled" ) - """# TODO: v3 api used `_representation` - New api is hiding it so it had to be renamed - to `representations_include` - """ representations_include: list[str] = Field( default_factory=list, title="Include representations" diff --git a/server_addon/nuke/server/settings/main.py b/server_addon/nuke/server/settings/main.py index 4687d48ac9..cdaaa3a9e2 100644 --- a/server_addon/nuke/server/settings/main.py +++ b/server_addon/nuke/server/settings/main.py @@ -59,9 +59,7 @@ class NukeSettings(BaseSettingsModel): default_factory=ImageIOSettings, title="Color Management (imageio)", ) - """# TODO: fix host api: - - rename `nuke-dirmap` to `dirmap` was inevitable - """ + dirmap: DirmapSettings = Field( default_factory=DirmapSettings, title="Nuke Directory Mapping", diff --git a/server_addon/nuke/server/settings/publish_plugins.py b/server_addon/nuke/server/settings/publish_plugins.py index de21870d5f..c78685534f 100644 --- a/server_addon/nuke/server/settings/publish_plugins.py +++ b/server_addon/nuke/server/settings/publish_plugins.py @@ -28,11 +28,9 @@ def nuke_product_types_enum(): class NodeModel(BaseSettingsModel): - # TODO: missing in host api name: str = Field( title="Node name" ) - # TODO: `nodeclass` rename to `nuke_node_class` nodeclass: str = Field( "", title="Node class" @@ -41,10 +39,6 @@ class NodeModel(BaseSettingsModel): "", title="Incoming dependency" ) - """# TODO: Changes in host api: - - Need complete rework of knob types in nuke integration. - - We could not support v3 style of settings. - """ knobs: list[KnobModel] = Field( default_factory=list, title="Knobs", @@ -100,10 +94,6 @@ class ExtractThumbnailModel(BaseSettingsModel): use_rendered: bool = Field(title="Use rendered images") bake_viewer_process: bool = Field(title="Bake view process") bake_viewer_input_process: bool = Field(title="Bake viewer input process") - """# TODO: needs to rewrite from v3 to ayon - - `nodes` in v3 was dict but now `prenodes` is list of dict - - also later `nodes` should be `prenodes` - """ nodes: list[NodeModel] = Field( default_factory=list, @@ -216,12 +206,6 @@ class ExctractSlateFrameParamModel(BaseSettingsModel): class ExtractSlateFrameModel(BaseSettingsModel): viewer_lut_raw: bool = Field(title="Viewer lut raw") - """# TODO: v3 api different model: - - not possible to replicate v3 model: - {"name": [bool, str]} - - not it is: - {"name": {"enabled": bool, "template": str}} - """ key_value_mapping: ExctractSlateFrameParamModel = Field( title="Key value mapping", default_factory=ExctractSlateFrameParamModel @@ -290,7 +274,6 @@ class PublishPuginsModel(BaseSettingsModel): title="Extract Slate Frame", default_factory=ExtractSlateFrameModel ) - # TODO: plugin should be renamed - `workfile` not `script` IncrementScriptVersion: IncrementScriptVersionModel = Field( title="Increment Workfile Version", default_factory=IncrementScriptVersionModel, diff --git a/server_addon/nuke/server/settings/scriptsmenu.py b/server_addon/nuke/server/settings/scriptsmenu.py index 9d1c32ebac..0b2d660da5 100644 --- a/server_addon/nuke/server/settings/scriptsmenu.py +++ b/server_addon/nuke/server/settings/scriptsmenu.py @@ -17,7 +17,6 @@ class ScriptsmenuSettings(BaseSettingsModel): """Nuke script menu project settings.""" _isGroup = True - # TODO: in api rename key `name` to `menu_name` name: str = Field(title="Menu Name") definition: list[ScriptsmenuSubmodel] = Field( default_factory=list, diff --git a/server_addon/nuke/server/settings/templated_workfile_build.py b/server_addon/nuke/server/settings/templated_workfile_build.py index e0245c8d06..0899be841e 100644 --- a/server_addon/nuke/server/settings/templated_workfile_build.py +++ b/server_addon/nuke/server/settings/templated_workfile_build.py @@ -28,6 +28,7 @@ class TemplatedWorkfileProfileModel(BaseSettingsModel): class TemplatedWorkfileBuildModel(BaseSettingsModel): + """Settings for templated workfile builder.""" profiles: list[TemplatedWorkfileProfileModel] = Field( default_factory=list ) diff --git a/server_addon/nuke/server/settings/workfile_builder.py b/server_addon/nuke/server/settings/workfile_builder.py index e9ac0db2a0..3ae3b08788 100644 --- a/server_addon/nuke/server/settings/workfile_builder.py +++ b/server_addon/nuke/server/settings/workfile_builder.py @@ -58,6 +58,8 @@ class BuilderProfileModel(BaseSettingsModel): class WorkfileBuilderModel(BaseSettingsModel): + """[deprecated] use Template Workfile Build Settings instead. + """ create_first_version: bool = Field( title="Create first workfile") custom_templates: list[CustomTemplateModel] = Field( From 0836b21116690fa1c7f11dfb0c5e24391a558d6a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 7 Sep 2023 14:23:51 +0200 Subject: [PATCH 320/327] Fix usage of `out_SET` and `controls_SET` since #5310 because they can now be prefixed - Collect the rig sets only once (I've ordered it before Collect History so that the instance still contains less node, as an optimization) - Also fixes a hard error when `out_SET` is not found, instead now only relevant `PublishValidationError` are raised to generate a nice report --- .../maya/plugins/publish/collect_rig_sets.py | 39 +++++++++++++++++ .../plugins/publish/validate_rig_contents.py | 42 +++++++++++++------ .../publish/validate_rig_controllers.py | 36 +++++++++++----- ...idate_rig_controllers_arnold_attributes.py | 6 +-- .../publish/validate_rig_out_set_node_ids.py | 11 +++-- .../publish/validate_rig_output_ids.py | 5 ++- 6 files changed, 108 insertions(+), 31 deletions(-) create mode 100644 openpype/hosts/maya/plugins/publish/collect_rig_sets.py diff --git a/openpype/hosts/maya/plugins/publish/collect_rig_sets.py b/openpype/hosts/maya/plugins/publish/collect_rig_sets.py new file mode 100644 index 0000000000..36a4211af1 --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/collect_rig_sets.py @@ -0,0 +1,39 @@ +import pyblish.api +from maya import cmds + + +class CollectRigSets(pyblish.api.InstancePlugin): + """Ensure rig contains pipeline-critical content + + Every rig must contain at least two object sets: + "controls_SET" - Set of all animatable controls + "out_SET" - Set of all cacheable meshes + + """ + + order = pyblish.api.CollectorOrder + 0.05 + label = "Collect Rig Sets" + hosts = ["maya"] + families = ["rig"] + + accepted_output = ["mesh", "transform"] + accepted_controllers = ["transform"] + + def process(self, instance): + + # Find required sets by suffix + searching = {"controls_SET", "out_SET"} + found = {} + for node in cmds.ls(instance, exactType="objectSet"): + for suffix in searching: + if node.endswith(suffix): + found[suffix] = node + searching.remove(suffix) + break + if not searching: + break + + self.log.debug("Found sets: {}".format(found)) + rig_sets = instance.data.setdefault("rig_sets", {}) + for name, objset in found.items(): + rig_sets[name] = objset diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_contents.py b/openpype/hosts/maya/plugins/publish/validate_rig_contents.py index 7b5392f8f9..23f031a5db 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_contents.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_contents.py @@ -2,7 +2,9 @@ import pyblish.api from maya import cmds from openpype.pipeline.publish import ( - PublishValidationError, ValidateContentsOrder) + PublishValidationError, + ValidateContentsOrder +) class ValidateRigContents(pyblish.api.InstancePlugin): @@ -24,31 +26,45 @@ class ValidateRigContents(pyblish.api.InstancePlugin): def process(self, instance): - objectsets = ("controls_SET", "out_SET") - missing = [obj for obj in objectsets if obj not in instance] - assert not missing, ("%s is missing %s" % (instance, missing)) + # Find required sets by suffix + required = ["controls_SET", "out_SET"] + missing = [ + key for key in required if key not in instance.data["rig_sets"] + ] + if missing: + raise PublishValidationError( + "%s is missing sets: %s" % (instance, ", ".join(missing)) + ) + + controls_set = instance.data["rig_sets"]["controls_SET"] + out_set = instance.data["rig_sets"]["out_SET"] # Ensure there are at least some transforms or dag nodes # in the rig instance set_members = instance.data['setMembers'] if not cmds.ls(set_members, type="dagNode", long=True): raise PublishValidationError( - ("No dag nodes in the pointcache instance. " - "(Empty instance?)")) + "No dag nodes in the pointcache instance. " + "(Empty instance?)" + ) # Ensure contents in sets and retrieve long path for all objects - output_content = cmds.sets("out_SET", query=True) or [] - assert output_content, "Must have members in rig out_SET" + output_content = cmds.sets(out_set, query=True) or [] + if not output_content: + raise PublishValidationError("Must have members in rig out_SET") output_content = cmds.ls(output_content, long=True) - controls_content = cmds.sets("controls_SET", query=True) or [] - assert controls_content, "Must have members in rig controls_SET" + controls_content = cmds.sets(controls_set, query=True) or [] + if not controls_content: + raise PublishValidationError( + "Must have members in rig controls_SET" + ) controls_content = cmds.ls(controls_content, long=True) # Validate members are inside the hierarchy from root node - root_node = cmds.ls(set_members, assemblies=True) - hierarchy = cmds.listRelatives(root_node, allDescendents=True, - fullPath=True) + root_nodes = cmds.ls(set_members, assemblies=True, long=True) + hierarchy = cmds.listRelatives(root_nodes, allDescendents=True, + fullPath=True) + root_nodes hierarchy = set(hierarchy) invalid_hierarchy = [] diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_controllers.py b/openpype/hosts/maya/plugins/publish/validate_rig_controllers.py index 7bbf4257ab..a3828f871b 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_controllers.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_controllers.py @@ -52,22 +52,30 @@ class ValidateRigControllers(pyblish.api.InstancePlugin): def process(self, instance): invalid = self.get_invalid(instance) if invalid: - raise PublishValidationError('{} failed, see log ' - 'information'.format(self.label)) + raise PublishValidationError( + '{} failed, see log information'.format(self.label) + ) @classmethod def get_invalid(cls, instance): - controllers_sets = [i for i in instance if i == "controls_SET"] - controls = cmds.sets(controllers_sets, query=True) - assert controls, "Must have 'controls_SET' in rig instance" + controls_set = instance.data["rig_sets"].get("controls_SET") + if not controls_set: + cls.log.error( + "Must have 'controls_SET' in rig instance" + ) + return [instance.data["instance_node"]] + + controls = cmds.sets(controls_set, query=True) # Ensure all controls are within the top group lookup = set(instance[:]) - assert all(control in lookup for control in cmds.ls(controls, - long=True)), ( - "All controls must be inside the rig's group." - ) + if not all(control in lookup for control in cmds.ls(controls, + long=True)): + cls.log.error( + "All controls must be inside the rig's group." + ) + return [controls_set] # Validate all controls has_connections = list() @@ -181,9 +189,17 @@ class ValidateRigControllers(pyblish.api.InstancePlugin): @classmethod def repair(cls, instance): + controls_set = instance.data["rig_sets"].get("controls_SET") + if not controls_set: + cls.log.error( + "Unable to repair because no 'controls_SET' found in rig " + "instance: {}".format(instance) + ) + return + # Use a single undo chunk with undo_chunk(): - controls = cmds.sets("controls_SET", query=True) + controls = cmds.sets(controls_set, query=True) for control in controls: # Lock visibility diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_controllers_arnold_attributes.py b/openpype/hosts/maya/plugins/publish/validate_rig_controllers_arnold_attributes.py index 842c1de01b..03f6a5f1ab 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_controllers_arnold_attributes.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_controllers_arnold_attributes.py @@ -56,11 +56,11 @@ class ValidateRigControllersArnoldAttributes(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance): - controllers_sets = [i for i in instance if i == "controls_SET"] - if not controllers_sets: + controls_set = instance.data["rig_sets"].get("controls_SET") + if not controls_set: return [] - controls = cmds.sets(controllers_sets, query=True) or [] + controls = cmds.sets(controls_set, query=True) or [] if not controls: return [] diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py b/openpype/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py index 39f0941faa..fbd510c683 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py @@ -38,16 +38,19 @@ class ValidateRigOutSetNodeIds(pyblish.api.InstancePlugin): # if a deformer has been created on the shape invalid = self.get_invalid(instance) if invalid: - raise PublishValidationError("Nodes found with mismatching " - "IDs: {0}".format(invalid)) + raise PublishValidationError( + "Nodes found with mismatching IDs: {0}".format(invalid) + ) @classmethod def get_invalid(cls, instance): """Get all nodes which do not match the criteria""" - invalid = [] + out_set = instance.data["rig_sets"].get("out_SET") + if not out_set: + return [] - out_set = next(x for x in instance if x.endswith("out_SET")) + invalid = [] members = cmds.sets(out_set, query=True) shapes = cmds.ls(members, dag=True, diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py b/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py index cbc750bace..24fb36eb8b 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py @@ -47,7 +47,10 @@ class ValidateRigOutputIds(pyblish.api.InstancePlugin): invalid = {} if compute: - out_set = next(x for x in instance if "out_SET" in x) + out_set = instance.data["rig_sets"].get("out_SET") + if not out_set: + instance.data["mismatched_output_ids"] = invalid + return invalid instance_nodes = cmds.sets(out_set, query=True, nodesOnly=True) instance_nodes = cmds.ls(instance_nodes, long=True) From a492addc54691fd23ce462cd06284809a3edeaa0 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 7 Sep 2023 15:55:28 +0200 Subject: [PATCH 321/327] Reduce artist-facing logs for component integration for Ftrack + tweak "Comment is not set" log also for Kitsu to debug level --- .../ftrack/plugins/publish/integrate_ftrack_api.py | 12 ++++++------ .../plugins/publish/integrate_ftrack_description.py | 2 +- .../ftrack/plugins/publish/integrate_ftrack_note.py | 6 +++--- .../kitsu/plugins/publish/integrate_kitsu_note.py | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py index 4d474fab10..858c0bb2d6 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py @@ -27,8 +27,8 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin): def process(self, instance): component_list = instance.data.get("ftrackComponentsList") if not component_list: - self.log.info( - "Instance don't have components to integrate to Ftrack." + self.log.debug( + "Instance doesn't have components to integrate to Ftrack." " Skipping." ) return @@ -37,7 +37,7 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin): task_entity, parent_entity = self.get_instance_entities( instance, context) if parent_entity is None: - self.log.info(( + self.log.debug(( "Skipping ftrack integration. Instance \"{}\" does not" " have specified ftrack entities." ).format(str(instance))) @@ -323,7 +323,7 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin): "type_id": asset_type_id, "context_id": parent_id } - self.log.info("Created new Asset with data: {}.".format(asset_data)) + self.log.debug("Created new Asset with data: {}.".format(asset_data)) session.create("Asset", asset_data) session.commit() return self._query_asset(session, asset_name, asset_type_id, parent_id) @@ -384,7 +384,7 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin): if comment: new_asset_version_data["comment"] = comment - self.log.info("Created new AssetVersion with data {}".format( + self.log.debug("Created new AssetVersion with data {}".format( new_asset_version_data )) session.create("AssetVersion", new_asset_version_data) @@ -555,7 +555,7 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin): location=location ) data["component"] = component_entity - self.log.info( + self.log.debug( ( "Created new Component with path: {0}, data: {1}," " metadata: {2}, location: {3}" diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_description.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_description.py index 6ed02bc8b6..ceaff8ff54 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_description.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_description.py @@ -40,7 +40,7 @@ class IntegrateFtrackDescription(pyblish.api.InstancePlugin): comment = instance.data["comment"] if not comment: - self.log.info("Comment is not set.") + self.log.debug("Comment is not set.") else: self.log.debug("Comment is set to `{}`".format(comment)) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_note.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_note.py index 6e82897d89..10b7932cdf 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_note.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_note.py @@ -47,7 +47,7 @@ class IntegrateFtrackNote(pyblish.api.InstancePlugin): app_label = context.data["appLabel"] comment = instance.data["comment"] if not comment: - self.log.info("Comment is not set.") + self.log.debug("Comment is not set.") else: self.log.debug("Comment is set to `{}`".format(comment)) @@ -127,14 +127,14 @@ class IntegrateFtrackNote(pyblish.api.InstancePlugin): note_text = StringTemplate.format_template(template, format_data) if not note_text.solved: - self.log.warning(( + self.log.debug(( "Note template require more keys then can be provided." "\nTemplate: {}\nMissing values for keys:{}\nData: {}" ).format(template, note_text.missing_keys, format_data)) continue if not note_text: - self.log.info(( + self.log.debug(( "Note for AssetVersion {} would be empty. Skipping." "\nTemplate: {}\nData: {}" ).format(asset_version["id"], template, format_data)) diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py index 6e5dd056f3..b66e1f01e0 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py @@ -121,7 +121,7 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): publish_comment = self.format_publish_comment(instance) if not publish_comment: - self.log.info("Comment is not set.") + self.log.debug("Comment is not set.") else: self.log.debug("Comment is `{}`".format(publish_comment)) From 3a02964af5e4c2c9aac5da4770e59aeef913f98a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 7 Sep 2023 15:58:28 +0200 Subject: [PATCH 322/327] Do not show debug log about ffmpeg probe in artist-facing report --- openpype/lib/transcoding.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index 2bae28786e..6e323f55c1 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -724,7 +724,7 @@ def get_ffprobe_data(path_to_file, logger=None): """ if not logger: logger = logging.getLogger(__name__) - logger.info( + logger.debug( "Getting information about input \"{}\".".format(path_to_file) ) ffprobe_args = get_ffmpeg_tool_args("ffprobe") From 2919d241da617229efa47c9ae52854633e0100c4 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 7 Sep 2023 19:30:10 +0300 Subject: [PATCH 323/327] move import inside --- .../hosts/houdini/plugins/create/create_review.py | 12 ++++-------- .../plugins/publish/validate_review_colorspace.py | 2 +- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_review.py b/openpype/hosts/houdini/plugins/create/create_review.py index c087c54f6c..60c34a358b 100644 --- a/openpype/hosts/houdini/plugins/create/create_review.py +++ b/openpype/hosts/houdini/plugins/create/create_review.py @@ -2,7 +2,6 @@ """Creator plugin for creating openGL reviews.""" from openpype.hosts.houdini.api import plugin from openpype.lib import EnumDef, BoolDef, NumberDef -from openpype.hosts.houdini.api.colorspace import get_default_display_view_colorspace # noqa import os import hou @@ -87,7 +86,8 @@ class CreateReview(plugin.HoudiniCreator): # Set OCIO Colorspace to the default output colorspace # if there's OCIO - self.set_colorcorrect_to_default_view_space(instance_node) + if os.getenv("OCIO"): + self.set_colorcorrect_to_default_view_space(instance_node) to_lock = ["id", "family"] @@ -134,13 +134,9 @@ class CreateReview(plugin.HoudiniCreator): def set_colorcorrect_to_default_view_space(self, instance_node): """Set ociocolorspace to the default output space.""" + from openpype.hosts.houdini.api.colorspace import get_default_display_view_colorspace # noqa - if os.getenv("OCIO") is None: - # No OCIO, skip setting ociocolorspace - return - - # if there's OCIO then set Color Correction parameter - # to OpenColorIO + # set Color Correction parameter to OpenColorIO instance_node.setParms({"colorcorrect": 2}) # Get default view space for ociocolorspace parm. diff --git a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py index 61c3a755d0..03ecd1b052 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py +++ b/openpype/hosts/houdini/plugins/publish/validate_review_colorspace.py @@ -6,7 +6,6 @@ from openpype.pipeline import ( ) from openpype.pipeline.publish import RepairAction from openpype.hosts.houdini.api.action import SelectROPAction -from openpype.hosts.houdini.api.colorspace import get_default_display_view_colorspace # noqa import os import hou @@ -69,6 +68,7 @@ class ValidateReviewColorspace(pyblish.api.InstancePlugin, It is a helper action more than a repair action, used to set colorspace on opengl node to the default view. """ + from openpype.hosts.houdini.api.colorspace import get_default_display_view_colorspace # noqa rop_node = hou.node(instance.data["instance_node"]) From d2f63f0cd489b490111c4fdba4916bfb2c1e2ad0 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 7 Sep 2023 23:27:30 +0200 Subject: [PATCH 324/327] Fix look assigner showing no meshes if 'not found' representations are present --- openpype/hosts/maya/tools/mayalookassigner/commands.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/tools/mayalookassigner/commands.py b/openpype/hosts/maya/tools/mayalookassigner/commands.py index a1290aa68d..5cc4f84931 100644 --- a/openpype/hosts/maya/tools/mayalookassigner/commands.py +++ b/openpype/hosts/maya/tools/mayalookassigner/commands.py @@ -138,8 +138,13 @@ def create_items_from_nodes(nodes): asset_doc = asset_docs_by_id.get(asset_id) # Skip if asset id is not found if not asset_doc: - log.warning("Id not found in the database, skipping '%s'." % _id) - log.warning("Nodes: %s" % id_nodes) + log.warning( + "Id found on {num} nodes for which no asset is found database," + " skipping '{asset_id}'".format( + num=len(nodes), + asset_id=asset_id + ) + ) continue # Collect available look subsets for this asset From 076d16a50d21ceb1edfbc806b20554a971c01b40 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 8 Sep 2023 13:45:00 +0200 Subject: [PATCH 325/327] Workfiles tool: Refactor workfiles tool (for AYON) (#5550) * ayon workfiles tool initial commit * separated models into smaller files * workfile can be listed and opened * added browse logic * added TODO for helper functions * modified abstract controller * implemented required methods * base of save dialog * added project settings to controller * set context of side panel on init * implemented save as dialog * cleanup expected selection * unify controller variable name * base of published workfiles * working published workfile copy * added more missing features from workfiles tool * Changed size policy of buttons to fill space vertically * added overlay messages * moved objects to abstraction * moved 'window.py' to widgets * small modifications in widgets * get_workfile_info returns object * filled docstrings in abstractions * finishing touches * backwards compatible work with host * close window on successfull open * remove indentation completelly * added style for overlay label * added handling of invalid host in controller * added overlay with message if host is not valid * added missing feature of disabled save * use ayon_workfiles in ayon mode * cleanup * hound fixes * use asset doc for 'change_current_context' * added duplication action * removed unused attributes and methods * refresh workarea view on save as finished * support host integrations without 'HostBase' * fix 'filepath' fill * reset item cache on save * do not handle filepath in prepare workfile * rename '_create_workfile_doc' > '_create_workfile_info_entity' * fill comment before formatting * fix column count by not calling 'clear' * more explicit name of method * use 'setHeaderData' to define header labels * mimic changes from workarea widget in published widget --- openpype/style/style.css | 4 + openpype/tools/ayon_workfiles/__init__.py | 0 openpype/tools/ayon_workfiles/abstract.py | 984 ++++++++++++++++++ openpype/tools/ayon_workfiles/control.py | 642 ++++++++++++ .../tools/ayon_workfiles/models/__init__.py | 10 + .../tools/ayon_workfiles/models/hierarchy.py | 225 ++++ .../tools/ayon_workfiles/models/selection.py | 91 ++ .../tools/ayon_workfiles/models/workfiles.py | 711 +++++++++++++ .../tools/ayon_workfiles/widgets/__init__.py | 6 + .../tools/ayon_workfiles/widgets/constants.py | 7 + .../ayon_workfiles/widgets/files_widget.py | 398 +++++++ .../widgets/files_widget_published.py | 378 +++++++ .../widgets/files_widget_workarea.py | 380 +++++++ .../ayon_workfiles/widgets/folders_widget.py | 324 ++++++ .../ayon_workfiles/widgets/save_as_dialog.py | 351 +++++++ .../ayon_workfiles/widgets/side_panel.py | 163 +++ .../ayon_workfiles/widgets/tasks_widget.py | 420 ++++++++ .../tools/ayon_workfiles/widgets/utils.py | 94 ++ .../tools/ayon_workfiles/widgets/window.py | 400 +++++++ openpype/tools/utils/host_tools.py | 30 +- 20 files changed, 5610 insertions(+), 8 deletions(-) create mode 100644 openpype/tools/ayon_workfiles/__init__.py create mode 100644 openpype/tools/ayon_workfiles/abstract.py create mode 100644 openpype/tools/ayon_workfiles/control.py create mode 100644 openpype/tools/ayon_workfiles/models/__init__.py create mode 100644 openpype/tools/ayon_workfiles/models/hierarchy.py create mode 100644 openpype/tools/ayon_workfiles/models/selection.py create mode 100644 openpype/tools/ayon_workfiles/models/workfiles.py create mode 100644 openpype/tools/ayon_workfiles/widgets/__init__.py create mode 100644 openpype/tools/ayon_workfiles/widgets/constants.py create mode 100644 openpype/tools/ayon_workfiles/widgets/files_widget.py create mode 100644 openpype/tools/ayon_workfiles/widgets/files_widget_published.py create mode 100644 openpype/tools/ayon_workfiles/widgets/files_widget_workarea.py create mode 100644 openpype/tools/ayon_workfiles/widgets/folders_widget.py create mode 100644 openpype/tools/ayon_workfiles/widgets/save_as_dialog.py create mode 100644 openpype/tools/ayon_workfiles/widgets/side_panel.py create mode 100644 openpype/tools/ayon_workfiles/widgets/tasks_widget.py create mode 100644 openpype/tools/ayon_workfiles/widgets/utils.py create mode 100644 openpype/tools/ayon_workfiles/widgets/window.py diff --git a/openpype/style/style.css b/openpype/style/style.css index 5ce55aa658..ca368f84f8 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -1427,6 +1427,10 @@ CreateNextPageOverlay { background: rgba(0, 0, 0, 127); } +#OverlayFrameLabel { + font-size: 15pt; +} + #BreadcrumbsPathInput { padding: 2px; font-size: 9pt; diff --git a/openpype/tools/ayon_workfiles/__init__.py b/openpype/tools/ayon_workfiles/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openpype/tools/ayon_workfiles/abstract.py b/openpype/tools/ayon_workfiles/abstract.py new file mode 100644 index 0000000000..e30a2c2499 --- /dev/null +++ b/openpype/tools/ayon_workfiles/abstract.py @@ -0,0 +1,984 @@ +import os +from abc import ABCMeta, abstractmethod + +import six +from openpype.style import get_default_entity_icon_color + + +class WorkfileInfo: + """Information about workarea file with possible additional from database. + + Args: + folder_id (str): Folder id. + task_id (str): Task id. + filepath (str): Filepath. + filesize (int): File size. + creation_time (int): Creation time (timestamp). + modification_time (int): Modification time (timestamp). + note (str): Note. + """ + + def __init__( + self, + folder_id, + task_id, + filepath, + filesize, + creation_time, + modification_time, + note, + ): + self.folder_id = folder_id + self.task_id = task_id + self.filepath = filepath + self.filesize = filesize + self.creation_time = creation_time + self.modification_time = modification_time + self.note = note + + def to_data(self): + """Converts WorkfileInfo item to data. + + Returns: + dict[str, Any]: Folder item data. + """ + + return { + "folder_id": self.folder_id, + "task_id": self.task_id, + "filepath": self.filepath, + "filesize": self.filesize, + "creation_time": self.creation_time, + "modification_time": self.modification_time, + "note": self.note, + } + + @classmethod + def from_data(cls, data): + """Re-creates WorkfileInfo item from data. + + Args: + data (dict[str, Any]): Workfile info item data. + + Returns: + WorkfileInfo: Workfile info item. + """ + + return cls(**data) + + +class FolderItem: + """Item representing folder entity on a server. + + Folder can be a child of another folder or a project. + + Args: + entity_id (str): Folder id. + parent_id (Union[str, None]): Parent folder id. If 'None' then project + is parent. + name (str): Name of folder. + label (str): Folder label. + icon_name (str): Name of icon from font awesome. + icon_color (str): Hex color string that will be used for icon. + """ + + def __init__( + self, entity_id, parent_id, name, label, icon_name, icon_color + ): + self.entity_id = entity_id + self.parent_id = parent_id + self.name = name + self.icon_name = icon_name or "fa.folder" + self.icon_color = icon_color or get_default_entity_icon_color() + self.label = label or name + + def to_data(self): + """Converts folder item to data. + + Returns: + dict[str, Any]: Folder item data. + """ + + return { + "entity_id": self.entity_id, + "parent_id": self.parent_id, + "name": self.name, + "label": self.label, + "icon_name": self.icon_name, + "icon_color": self.icon_color, + } + + @classmethod + def from_data(cls, data): + """Re-creates folder item from data. + + Args: + data (dict[str, Any]): Folder item data. + + Returns: + FolderItem: Folder item. + """ + + return cls(**data) + + +class TaskItem: + """Task item representing task entity on a server. + + Task is child of a folder. + + Task item has label that is used for display in UI. The label is by + default using task name and type. + + Args: + task_id (str): Task id. + name (str): Name of task. + task_type (str): Type of task. + parent_id (str): Parent folder id. + icon_name (str): Name of icon from font awesome. + icon_color (str): Hex color string that will be used for icon. + """ + + def __init__( + self, task_id, name, task_type, parent_id, icon_name, icon_color + ): + self.task_id = task_id + self.name = name + self.task_type = task_type + self.parent_id = parent_id + self.icon_name = icon_name or "fa.male" + self.icon_color = icon_color or get_default_entity_icon_color() + self._label = None + + @property + def id(self): + """Alias for task_id. + + Returns: + str: Task id. + """ + + return self.task_id + + @property + def label(self): + """Label of task item for UI. + + Returns: + str: Label of task item. + """ + + if self._label is None: + self._label = "{} ({})".format(self.name, self.task_type) + return self._label + + def to_data(self): + """Converts task item to data. + + Returns: + dict[str, Any]: Task item data. + """ + + return { + "task_id": self.task_id, + "name": self.name, + "parent_id": self.parent_id, + "task_type": self.task_type, + "icon_name": self.icon_name, + "icon_color": self.icon_color, + } + + @classmethod + def from_data(cls, data): + """Re-create task item from data. + + Args: + data (dict[str, Any]): Task item data. + + Returns: + TaskItem: Task item. + """ + + return cls(**data) + + +class FileItem: + """File item that represents a file. + + Can be used for both Workarea and Published workfile. Workarea file + will always exist on disk which is not the case for Published workfile. + + Args: + dirpath (str): Directory path of file. + filename (str): Filename. + modified (float): Modified timestamp. + representation_id (Optional[str]): Representation id of published + workfile. + filepath (Optional[str]): Prepared filepath. + exists (Optional[bool]): If file exists on disk. + """ + + def __init__( + self, + dirpath, + filename, + modified, + representation_id=None, + filepath=None, + exists=None + ): + self.filename = filename + self.dirpath = dirpath + self.modified = modified + self.representation_id = representation_id + self._filepath = filepath + self._exists = exists + + @property + def filepath(self): + """Filepath of file. + + Returns: + str: Full path to a file. + """ + + if self._filepath is None: + self._filepath = os.path.join(self.dirpath, self.filename) + return self._filepath + + @property + def exists(self): + """File is available. + + Returns: + bool: If file exists on disk. + """ + + if self._exists is None: + self._exists = os.path.exists(self.filepath) + return self._exists + + def to_data(self): + """Converts file item to data. + + Returns: + dict[str, Any]: File item data. + """ + + return { + "filename": self.filename, + "dirpath": self.dirpath, + "modified": self.modified, + "representation_id": self.representation_id, + "filepath": self.filepath, + "exists": self.exists, + } + + @classmethod + def from_data(cls, data): + """Re-creates file item from data. + + Args: + data (dict[str, Any]): File item data. + + Returns: + FileItem: File item. + """ + + required_keys = { + "filename", + "dirpath", + "modified", + "representation_id" + } + missing_keys = required_keys - set(data.keys()) + if missing_keys: + raise KeyError("Missing keys: {}".format(missing_keys)) + + return cls(**{ + key: data[key] + for key in required_keys + }) + + +class WorkareaFilepathResult: + """Result of workarea file formatting. + + Args: + root (str): Root path of workarea. + filename (str): Filename. + exists (bool): True if file exists. + filepath (str): Filepath. If not provided it will be constructed + from root and filename. + """ + + def __init__(self, root, filename, exists, filepath=None): + if not filepath and root and filename: + filepath = os.path.join(root, filename) + self.root = root + self.filename = filename + self.exists = exists + self.filepath = filepath + + +@six.add_metaclass(ABCMeta) +class AbstractWorkfilesCommon(object): + @abstractmethod + def is_host_valid(self): + """Host is valid for workfiles tool work. + + Returns: + bool: True if host is valid. + """ + + pass + + @abstractmethod + def get_workfile_extensions(self): + """Get possible workfile extensions. + + Defined by host implementation. + + Returns: + Iterable[str]: List of extensions. + """ + + pass + + @abstractmethod + def is_save_enabled(self): + """Is workfile save enabled. + + Returns: + bool: True if save is enabled. + """ + + pass + + @abstractmethod + def set_save_enabled(self, enabled): + """Enable or disabled workfile save. + + Args: + enabled (bool): Enable save workfile when True. + """ + + pass + + +class AbstractWorkfilesBackend(AbstractWorkfilesCommon): + # Current context + @abstractmethod + def get_host_name(self): + """Name of host. + + Returns: + str: Name of host. + """ + pass + + @abstractmethod + def get_current_project_name(self): + """Project name from current context of host. + + Returns: + str: Name of project. + """ + + pass + + @abstractmethod + def get_current_folder_id(self): + """Folder id from current context of host. + + Returns: + Union[str, None]: Folder id or None if host does not have + any context. + """ + + pass + + @abstractmethod + def get_current_task_name(self): + """Task name from current context of host. + + Returns: + Union[str, None]: Task name or None if host does not have + any context. + """ + + pass + + @abstractmethod + def get_current_workfile(self): + """Current workfile from current context of host. + + Returns: + Union[str, None]: Path to workfile or None if host does + not have opened specific file. + """ + + pass + + @property + @abstractmethod + def project_anatomy(self): + """Project anatomy for current project. + + Returns: + Anatomy: Project anatomy. + """ + + pass + + @property + @abstractmethod + def project_settings(self): + """Project settings for current project. + + Returns: + dict[str, Any]: Project settings. + """ + + pass + + @abstractmethod + def get_folder_entity(self, folder_id): + """Get folder entity by id. + + Args: + folder_id (str): Folder id. + + Returns: + dict[str, Any]: Folder entity data. + """ + + pass + + @abstractmethod + def get_task_entity(self, task_id): + """Get task entity by id. + + Args: + task_id (str): Task id. + + Returns: + dict[str, Any]: Task entity data. + """ + + pass + + def emit_event(self, topic, data=None, source=None): + """Emit event. + + Args: + topic (str): Event topic used for callbacks filtering. + data (Optional[dict[str, Any]]): Event data. + source (Optional[str]): Event source. + """ + + pass + + +class AbstractWorkfilesFrontend(AbstractWorkfilesCommon): + """UI controller abstraction that is used for workfiles tool frontend. + + Abstraction to provide data for UI and to handle UI events. + + Provide access to abstract backend data, like folders and tasks. Cares + about handling of selection, keep information about current UI selection + and have ability to tell what selection should UI show. + + Selection is separated into 2 parts, first is what UI elements tell + about selection, and second is what UI should show as selected. + """ + + @abstractmethod + def register_event_callback(self, topic, callback): + """Register event callback. + + Listen for events with given topic. + + Args: + topic (str): Name of topic. + callback (Callable): Callback that will be called when event + is triggered. + """ + + pass + + # Host information + @abstractmethod + def get_workfile_extensions(self): + """Each host can define extensions that can be used for workfile. + + Returns: + List[str]: File extensions that can be used as workfile for + current host. + """ + + pass + + # Selection information + @abstractmethod + def get_selected_folder_id(self): + """Currently selected folder id. + + Returns: + Union[str, None]: Folder id or None if no folder is selected. + """ + + pass + + @abstractmethod + def set_selected_folder(self, folder_id): + """Change selected folder. + + This deselects currently selected task. + + Args: + folder_id (Union[str, None]): Folder id or None if no folder + is selected. + """ + + pass + + @abstractmethod + def get_selected_task_id(self): + """Currently selected task id. + + Returns: + Union[str, None]: Task id or None if no folder is selected. + """ + + pass + + @abstractmethod + def get_selected_task_name(self): + """Currently selected task name. + + Returns: + Union[str, None]: Task name or None if no folder is selected. + """ + + pass + + @abstractmethod + def set_selected_task(self, folder_id, task_id, task_name): + """Change selected task. + + Args: + folder_id (Union[str, None]): Folder id or None if no folder + is selected. + task_id (Union[str, None]): Task id or None if no task + is selected. + task_name (Union[str, None]): Task name or None if no task + is selected. + """ + + pass + + @abstractmethod + def get_selected_workfile_path(self): + """Currently selected workarea workile. + + Returns: + Union[str, None]: Selected workfile path. + """ + + pass + + @abstractmethod + def set_selected_workfile_path(self, path): + """Change selected workfile path. + + Args: + path (Union[str, None]): Selected workfile path. + """ + + pass + + @abstractmethod + def get_selected_representation_id(self): + """Currently selected workfile representation id. + + Returns: + Union[str, None]: Representation id or None if no representation + is selected. + """ + + pass + + @abstractmethod + def set_selected_representation_id(self, representation_id): + """Change selected representation. + + Args: + representation_id (Union[str, None]): Selected workfile + representation id. + """ + + pass + + def get_selected_context(self): + """Obtain selected context. + + Returns: + dict[str, Union[str, None]]: Selected context. + """ + + return { + "folder_id": self.get_selected_folder_id(), + "task_id": self.get_selected_task_id(), + "task_name": self.get_selected_task_name(), + "workfile_path": self.get_selected_workfile_path(), + "representation_id": self.get_selected_representation_id(), + } + + # Expected selection + # - expected selection is used to restore selection after refresh + # or when current context should be used + @abstractmethod + def set_expected_selection( + self, + folder_id, + task_name, + workfile_name=None, + representation_id=None + ): + """Define what should be selected in UI. + + Expected selection provide a way to define/change selection of + sequential UI elements. For example, if folder and task should be + selected a task element should wait until folder element has selected + folder. + + Triggers 'expected_selection.changed' event. + + Args: + folder_id (str): Folder id. + task_name (str): Task name. + workfile_name (Optional[str]): Workfile name. Used for workarea + files UI element. + representation_id (Optional[str]): Representation id. Used for + published filed UI element. + """ + + pass + + @abstractmethod + def get_expected_selection_data(self): + """Data of expected selection. + + TODOs: + Return defined object instead of dict. + + Returns: + dict[str, Any]: Expected selection data. + """ + + pass + + @abstractmethod + def expected_folder_selected(self, folder_id): + """Expected folder was selected in UI. + + Args: + folder_id (str): Folder id which was selected. + """ + + pass + + @abstractmethod + def expected_task_selected(self, folder_id, task_name): + """Expected task was selected in UI. + + Args: + folder_id (str): Folder id under which task is. + task_name (str): Task name which was selected. + """ + + pass + + @abstractmethod + def expected_representation_selected(self, representation_id): + """Expected representation was selected in UI. + + Args: + representation_id (str): Representation id which was selected. + """ + + pass + + @abstractmethod + def expected_workfile_selected(self, workfile_path): + """Expected workfile was selected in UI. + + Args: + workfile_path (str): Workfile path which was selected. + """ + + pass + + @abstractmethod + def go_to_current_context(self): + """Set expected selection to current context.""" + + pass + + # Model functions + @abstractmethod + def get_folder_items(self, sender): + """Folder items to visualize project hierarchy. + + This function may trigger events 'folders.refresh.started' and + 'folders.refresh.finished' which will contain 'sender' value in data. + That may help to avoid re-refresh of folder items in UI elements. + + Args: + sender (str): Who requested folder items. + + Returns: + list[FolderItem]: Minimum possible information needed + for visualisation of folder hierarchy. + """ + + pass + + @abstractmethod + def get_task_items(self, folder_id, sender): + """Task items. + + This function may trigger events 'tasks.refresh.started' and + 'tasks.refresh.finished' which will contain 'sender' value in data. + That may help to avoid re-refresh of task items in UI elements. + + Args: + folder_id (str): Folder ID for which are tasks requested. + sender (str): Who requested folder items. + + Returns: + list[TaskItem]: Minimum possible information needed + for visualisation of tasks. + """ + + pass + + @abstractmethod + def has_unsaved_changes(self): + """Has host unsaved change in currently running session. + + Returns: + bool: Has unsaved changes. + """ + + pass + + @abstractmethod + def get_workarea_dir_by_context(self, folder_id, task_id): + """Get workarea directory by context. + + Args: + folder_id (str): Folder id. + task_id (str): Task id. + + Returns: + str: Workarea directory. + """ + + pass + + @abstractmethod + def get_workarea_file_items(self, folder_id, task_id): + """Get workarea file items. + + Args: + folder_id (str): Folder id. + task_id (str): Task id. + + Returns: + list[FileItem]: List of workarea file items. + """ + + pass + + @abstractmethod + def get_workarea_save_as_data(self, folder_id, task_id): + """Prepare data for Save As operation. + + Todos: + Return defined object instead of dict. + + Args: + folder_id (str): Folder id. + task_id (str): Task id. + + Returns: + dict[str, Any]: Data for Save As operation. + """ + + pass + + @abstractmethod + def fill_workarea_filepath( + self, + folder_id, + task_id, + extension, + use_last_version, + version, + comment, + ): + """Calculate workfile path for passed context. + + Args: + folder_id (str): Folder id. + task_id (str): Task id. + extension (str): File extension. + use_last_version (bool): Use last version. + version (int): Version used if 'use_last_version' if 'False'. + comment (str): User's comment (subversion). + + Returns: + WorkareaFilepathResult: Result of the operation. + """ + + pass + + @abstractmethod + def get_published_file_items(self, folder_id, task_id): + """Get published file items. + + Args: + folder_id (str): Folder id. + task_id (Union[str, None]): Task id. + + Returns: + list[FileItem]: List of published file items. + """ + + pass + + @abstractmethod + def get_workfile_info(self, folder_id, task_id, filepath): + """Workfile info from database. + + Args: + folder_id (str): Folder id. + task_id (str): Task id. + filepath (str): Workfile path. + + Returns: + Union[WorkfileInfo, None]: Workfile info or None if was passed + invalid context. + """ + + pass + + @abstractmethod + def save_workfile_info(self, folder_id, task_id, filepath, note): + """Save workfile info to database. + + At this moment the only information which can be saved about + workfile is 'note'. + + Args: + folder_id (str): Folder id. + task_id (str): Task id. + filepath (str): Workfile path. + note (str): Note. + """ + + pass + + # General commands + @abstractmethod + def refresh(self): + """Refresh everything, models, ui etc. + + Triggers 'controller.refresh.started' event at the beginning and + 'controller.refresh.finished' at the end. + """ + + pass + + # Controller actions + @abstractmethod + def open_workfile(self, filepath): + """Open a workfile. + + Args: + filepath (str): Workfile path. + """ + + pass + + @abstractmethod + def save_current_workfile(self): + """Save state of current workfile.""" + + pass + + @abstractmethod + def save_as_workfile( + self, + folder_id, + task_id, + workdir, + filename, + template_key, + ): + """Save current state of workfile to workarea. + + Args: + folder_id (str): Folder id. + task_id (str): Task id. + workdir (str): Workarea directory. + filename (str): Workarea filename. + template_key (str): Template key used to get the workdir + and filename. + """ + + pass + + @abstractmethod + def copy_workfile_representation( + self, + representation_id, + representation_filepath, + folder_id, + task_id, + workdir, + filename, + template_key, + ): + """Action to copy published workfile representation to workarea. + + Triggers 'copy_representation.started' event on start and + 'copy_representation.finished' event with '{"failed": bool}'. + + Args: + representation_id (str): Representation id. + representation_filepath (str): Path to representation file. + folder_id (str): Folder id. + task_id (str): Task id. + workdir (str): Workarea directory. + filename (str): Workarea filename. + template_key (str): Template key. + """ + + pass + + @abstractmethod + def duplicate_workfile(self, src_filepath, workdir, filename): + """Duplicate workfile. + + Workfiles is not opened when done. + + Args: + src_filepath (str): Source workfile path. + workdir (str): Destination workdir. + filename (str): Destination filename. + """ + + pass diff --git a/openpype/tools/ayon_workfiles/control.py b/openpype/tools/ayon_workfiles/control.py new file mode 100644 index 0000000000..fc8819bff3 --- /dev/null +++ b/openpype/tools/ayon_workfiles/control.py @@ -0,0 +1,642 @@ +import os +import shutil + +import ayon_api + +from openpype.client import get_asset_by_id +from openpype.host import IWorkfileHost +from openpype.lib import Logger, emit_event +from openpype.lib.events import QueuedEventSystem +from openpype.settings import get_project_settings +from openpype.pipeline import Anatomy, registered_host +from openpype.pipeline.context_tools import ( + change_current_context, + get_current_host_name, + get_global_context, +) +from openpype.pipeline.workfile import create_workdir_extra_folders + +from .abstract import ( + AbstractWorkfilesFrontend, + AbstractWorkfilesBackend, +) +from .models import SelectionModel, EntitiesModel, WorkfilesModel + + +class ExpectedSelection: + def __init__(self): + self._folder_id = None + self._task_name = None + self._workfile_name = None + self._representation_id = None + self._folder_selected = True + self._task_selected = True + self._workfile_name_selected = True + self._representation_id_selected = True + + def set_expected_selection( + self, + folder_id, + task_name, + workfile_name=None, + representation_id=None + ): + self._folder_id = folder_id + self._task_name = task_name + self._workfile_name = workfile_name + self._representation_id = representation_id + self._folder_selected = False + self._task_selected = False + self._workfile_name_selected = workfile_name is None + self._representation_id_selected = representation_id is None + + def get_expected_selection_data(self): + return { + "folder_id": self._folder_id, + "task_name": self._task_name, + "workfile_name": self._workfile_name, + "representation_id": self._representation_id, + "folder_selected": self._folder_selected, + "task_selected": self._task_selected, + "workfile_name_selected": self._workfile_name_selected, + "representation_id_selected": self._representation_id_selected, + } + + def is_expected_folder_selected(self, folder_id): + return folder_id == self._folder_id and self._folder_selected + + def is_expected_task_selected(self, folder_id, task_name): + if not self.is_expected_folder_selected(folder_id): + return False + return task_name == self._task_name and self._task_selected + + def expected_folder_selected(self, folder_id): + if folder_id != self._folder_id: + return False + self._folder_selected = True + return True + + def expected_task_selected(self, folder_id, task_name): + if not self.is_expected_folder_selected(folder_id): + return False + + if task_name != self._task_name: + return False + + self._task_selected = True + return True + + def expected_workfile_selected(self, folder_id, task_name, workfile_name): + if not self.is_expected_task_selected(folder_id, task_name): + return False + + if workfile_name != self._workfile_name: + return False + self._workfile_name_selected = True + return True + + def expected_representation_selected( + self, folder_id, task_name, representation_id + ): + if not self.is_expected_task_selected(folder_id, task_name): + return False + if representation_id != self._representation_id: + return False + self._representation_id_selected = True + return True + + +class BaseWorkfileController( + AbstractWorkfilesFrontend, AbstractWorkfilesBackend +): + def __init__(self, host=None): + if host is None: + host = registered_host() + + host_is_valid = False + if host is not None: + missing_methods = ( + IWorkfileHost.get_missing_workfile_methods(host) + ) + host_is_valid = len(missing_methods) == 0 + + self._host = host + self._host_is_valid = host_is_valid + + self._project_anatomy = None + self._project_settings = None + self._event_system = None + self._log = None + + self._current_project_name = None + self._current_folder_name = None + self._current_folder_id = None + self._current_task_name = None + self._save_is_enabled = True + + # Expected selected folder and task + self._expected_selection = self._create_expected_selection_obj() + + self._selection_model = self._create_selection_model() + self._entities_model = self._create_entities_model() + self._workfiles_model = self._create_workfiles_model() + + @property + def log(self): + if self._log is None: + self._log = Logger.get_logger("WorkfilesUI") + return self._log + + def is_host_valid(self): + return self._host_is_valid + + def _create_expected_selection_obj(self): + return ExpectedSelection() + + def _create_selection_model(self): + return SelectionModel(self) + + def _create_entities_model(self): + return EntitiesModel(self) + + def _create_workfiles_model(self): + return WorkfilesModel(self) + + @property + def event_system(self): + """Inner event system for workfiles tool controller. + + Is used for communication with UI. Event system is created on demand. + + Returns: + QueuedEventSystem: Event system which can trigger callbacks + for topics. + """ + + if self._event_system is None: + self._event_system = QueuedEventSystem() + return self._event_system + + # ---------------------------------------------------- + # Implementation of methods required for backend logic + # ---------------------------------------------------- + @property + def project_settings(self): + if self._project_settings is None: + self._project_settings = get_project_settings( + self.get_current_project_name()) + return self._project_settings + + @property + def project_anatomy(self): + if self._project_anatomy is None: + self._project_anatomy = Anatomy(self.get_current_project_name()) + return self._project_anatomy + + def get_folder_entity(self, folder_id): + return self._entities_model.get_folder_entity(folder_id) + + def get_task_entity(self, task_id): + return self._entities_model.get_task_entity(task_id) + + # --------------------------------- + # Implementation of abstract methods + # --------------------------------- + def emit_event(self, topic, data=None, source=None): + """Use implemented event system to trigger event.""" + + if data is None: + data = {} + self.event_system.emit(topic, data, source) + + def register_event_callback(self, topic, callback): + self.event_system.add_callback(topic, callback) + + def is_save_enabled(self): + """Is workfile save enabled. + + Returns: + bool: True if save is enabled. + """ + + return self._save_is_enabled + + def set_save_enabled(self, enabled): + """Enable or disabled workfile save. + + Args: + enabled (bool): Enable save workfile when True. + """ + + if self._save_is_enabled == enabled: + return + + self._save_is_enabled = enabled + self._emit_event( + "workfile_save_enable.changed", + {"enabled": enabled} + ) + + # Host information + def get_workfile_extensions(self): + host = self._host + if isinstance(host, IWorkfileHost): + return host.get_workfile_extensions() + return host.file_extensions() + + def has_unsaved_changes(self): + host = self._host + if isinstance(host, IWorkfileHost): + return host.workfile_has_unsaved_changes() + return host.has_unsaved_changes() + + # Current context + def get_host_name(self): + host = self._host + if isinstance(host, IWorkfileHost): + return host.name + return get_current_host_name() + + def _get_host_current_context(self): + if hasattr(self._host, "get_current_context"): + return self._host.get_current_context() + return get_global_context() + + def get_current_project_name(self): + return self._current_project_name + + def get_current_folder_id(self): + return self._current_folder_id + + def get_current_task_name(self): + return self._current_task_name + + def get_current_workfile(self): + host = self._host + if isinstance(host, IWorkfileHost): + return host.get_current_workfile() + return host.current_file() + + # Selection information + def get_selected_folder_id(self): + return self._selection_model.get_selected_folder_id() + + def set_selected_folder(self, folder_id): + self._selection_model.set_selected_folder(folder_id) + + def get_selected_task_id(self): + return self._selection_model.get_selected_task_id() + + def get_selected_task_name(self): + return self._selection_model.get_selected_task_name() + + def set_selected_task(self, folder_id, task_id, task_name): + return self._selection_model.set_selected_task( + folder_id, task_id, task_name) + + def get_selected_workfile_path(self): + return self._selection_model.get_selected_workfile_path() + + def set_selected_workfile_path(self, path): + self._selection_model.set_selected_workfile_path(path) + + def get_selected_representation_id(self): + return self._selection_model.get_selected_representation_id() + + def set_selected_representation_id(self, representation_id): + self._selection_model.set_selected_representation_id( + representation_id) + + def set_expected_selection( + self, + folder_id, + task_name, + workfile_name=None, + representation_id=None + ): + self._expected_selection.set_expected_selection( + folder_id, task_name, workfile_name, representation_id + ) + self._trigger_expected_selection_changed() + + def expected_folder_selected(self, folder_id): + if self._expected_selection.expected_folder_selected(folder_id): + self._trigger_expected_selection_changed() + + def expected_task_selected(self, folder_id, task_name): + if self._expected_selection.expected_task_selected( + folder_id, task_name + ): + self._trigger_expected_selection_changed() + + def expected_workfile_selected(self, folder_id, task_name, workfile_name): + if self._expected_selection.expected_workfile_selected( + folder_id, task_name, workfile_name + ): + self._trigger_expected_selection_changed() + + def expected_representation_selected( + self, folder_id, task_name, representation_id + ): + if self._expected_selection.expected_representation_selected( + folder_id, task_name, representation_id + ): + self._trigger_expected_selection_changed() + + def get_expected_selection_data(self): + return self._expected_selection.get_expected_selection_data() + + def go_to_current_context(self): + self.set_expected_selection( + self._current_folder_id, self._current_task_name + ) + + # Model functions + def get_folder_items(self, sender): + return self._entities_model.get_folder_items(sender) + + def get_task_items(self, folder_id, sender): + return self._entities_model.get_tasks_items(folder_id, sender) + + def get_workarea_dir_by_context(self, folder_id, task_id): + return self._workfiles_model.get_workarea_dir_by_context( + folder_id, task_id) + + def get_workarea_file_items(self, folder_id, task_id): + return self._workfiles_model.get_workarea_file_items( + folder_id, task_id) + + def get_workarea_save_as_data(self, folder_id, task_id): + return self._workfiles_model.get_workarea_save_as_data( + folder_id, task_id) + + def fill_workarea_filepath( + self, + folder_id, + task_id, + extension, + use_last_version, + version, + comment, + ): + return self._workfiles_model.fill_workarea_filepath( + folder_id, + task_id, + extension, + use_last_version, + version, + comment, + ) + + def get_published_file_items(self, folder_id, task_id): + task_name = None + if task_id: + task = self.get_task_entity(task_id) + task_name = task.get("name") + + return self._workfiles_model.get_published_file_items( + folder_id, task_name) + + def get_workfile_info(self, folder_id, task_id, filepath): + return self._workfiles_model.get_workfile_info( + folder_id, task_id, filepath + ) + + def save_workfile_info(self, folder_id, task_id, filepath, note): + self._workfiles_model.save_workfile_info( + folder_id, task_id, filepath, note + ) + + def refresh(self): + if not self._host_is_valid: + self._emit_event("controller.refresh.started") + self._emit_event("controller.refresh.finished") + return + expected_folder_id = self.get_selected_folder_id() + expected_task_name = self.get_selected_task_name() + + self._emit_event("controller.refresh.started") + + context = self._get_host_current_context() + + project_name = context["project_name"] + folder_name = context["asset_name"] + task_name = context["task_name"] + folder_id = None + if folder_name: + folder = ayon_api.get_folder_by_name(project_name, folder_name) + if folder: + folder_id = folder["id"] + + self._project_settings = None + self._project_anatomy = None + + self._current_project_name = project_name + self._current_folder_name = folder_name + self._current_folder_id = folder_id + self._current_task_name = task_name + + if not expected_folder_id: + expected_folder_id = folder_id + expected_task_name = task_name + + self._expected_selection.set_expected_selection( + expected_folder_id, expected_task_name + ) + + self._entities_model.refresh() + + self._emit_event("controller.refresh.finished") + + # Controller actions + def open_workfile(self, filepath): + self._emit_event("open_workfile.started") + + failed = False + try: + self._host_open_workfile(filepath) + + except Exception: + failed = True + self.log.warning("Open of workfile failed", exc_info=True) + + self._emit_event( + "open_workfile.finished", + {"failed": failed}, + ) + + def save_current_workfile(self): + current_file = self.get_current_workfile() + self._host_save_workfile(current_file) + + def save_as_workfile( + self, + folder_id, + task_id, + workdir, + filename, + template_key, + ): + self._emit_event("save_as.started") + + failed = False + try: + self._save_as_workfile( + folder_id, + task_id, + workdir, + filename, + template_key, + ) + except Exception: + failed = True + self.log.warning("Save as failed", exc_info=True) + + self._emit_event( + "save_as.finished", + {"failed": failed}, + ) + + def copy_workfile_representation( + self, + representation_id, + representation_filepath, + folder_id, + task_id, + workdir, + filename, + template_key, + ): + self._emit_event("copy_representation.started") + + failed = False + try: + self._save_as_workfile( + folder_id, + task_id, + workdir, + filename, + template_key, + ) + except Exception: + failed = True + self.log.warning( + "Copy of workfile representation failed", exc_info=True + ) + + self._emit_event( + "copy_representation.finished", + {"failed": failed}, + ) + + def duplicate_workfile(self, src_filepath, workdir, filename): + self._emit_event("workfile_duplicate.started") + + failed = False + try: + dst_filepath = os.path.join(workdir, filename) + shutil.copy(src_filepath, dst_filepath) + except Exception: + failed = True + self.log.warning("Duplication of workfile failed", exc_info=True) + + self._emit_event( + "workfile_duplicate.finished", + {"failed": failed}, + ) + + # Helper host methods that resolve 'IWorkfileHost' interface + def _host_open_workfile(self, filepath): + host = self._host + if isinstance(host, IWorkfileHost): + host.open_workfile(filepath) + else: + host.open_file(filepath) + + def _host_save_workfile(self, filepath): + host = self._host + if isinstance(host, IWorkfileHost): + host.save_workfile(filepath) + else: + host.save_file(filepath) + + def _emit_event(self, topic, data=None): + self.emit_event(topic, data, "controller") + + # Expected selection + # - expected selection is used to restore selection after refresh + # or when current context should be used + def _trigger_expected_selection_changed(self): + self._emit_event( + "expected_selection_changed", + self._expected_selection.get_expected_selection_data(), + ) + + def _save_as_workfile( + self, + folder_id, + task_id, + workdir, + filename, + template_key, + src_filepath=None, + ): + # Trigger before save event + project_name = self.get_current_project_name() + folder = self.get_folder_entity(folder_id) + task = self.get_task_entity(task_id) + task_name = task["name"] + + # QUESTION should the data be different for 'before' and 'after'? + # NOTE keys should be OpenPype compatible + event_data = { + "project_name": project_name, + "folder_id": folder_id, + "asset_id": folder_id, + "asset_name": folder["name"], + "task_id": task_id, + "task_name": task_name, + "host_name": self.get_host_name(), + "filename": filename, + "workdir_path": workdir, + } + emit_event("workfile.save.before", event_data, source="workfiles.tool") + + # Create workfiles root folder + if not os.path.exists(workdir): + self.log.debug("Initializing work directory: %s", workdir) + os.makedirs(workdir) + + # Change context + if ( + folder_id != self.get_current_folder_id() + or task_name != self.get_current_task_name() + ): + # Use OpenPype asset-like object + asset_doc = get_asset_by_id(project_name, folder["id"]) + change_current_context( + asset_doc, + task["name"], + template_key=template_key + ) + + # Save workfile + dst_filepath = os.path.join(workdir, filename) + if src_filepath: + shutil.copyfile(src_filepath, dst_filepath) + self._host_open_workfile(dst_filepath) + else: + self._host_save_workfile(dst_filepath) + + # Create extra folders + create_workdir_extra_folders( + workdir, + self.get_host_name(), + task["taskType"], + task_name, + project_name + ) + + # Trigger after save events + emit_event("workfile.save.after", event_data, source="workfiles.tool") + self.refresh() diff --git a/openpype/tools/ayon_workfiles/models/__init__.py b/openpype/tools/ayon_workfiles/models/__init__.py new file mode 100644 index 0000000000..d906b9e7bd --- /dev/null +++ b/openpype/tools/ayon_workfiles/models/__init__.py @@ -0,0 +1,10 @@ +from .hierarchy import EntitiesModel +from .selection import SelectionModel +from .workfiles import WorkfilesModel + + +__all__ = ( + "SelectionModel", + "EntitiesModel", + "WorkfilesModel", +) diff --git a/openpype/tools/ayon_workfiles/models/hierarchy.py b/openpype/tools/ayon_workfiles/models/hierarchy.py new file mode 100644 index 0000000000..948c0b8a17 --- /dev/null +++ b/openpype/tools/ayon_workfiles/models/hierarchy.py @@ -0,0 +1,225 @@ +"""Hierarchy model that handles folders and tasks. + +The model can be extracted for common usage. In that case it will be required +to add more handling of project name changes. +""" + +import time +import collections +import contextlib + +import ayon_api + +from openpype.tools.ayon_workfiles.abstract import ( + FolderItem, + TaskItem, +) + + +def _get_task_items_from_tasks(tasks): + """ + + Returns: + TaskItem: Task item. + """ + + output = [] + for task in tasks: + folder_id = task["folderId"] + output.append(TaskItem( + task["id"], + task["name"], + task["type"], + folder_id, + None, + None + )) + return output + + +def _get_folder_item_from_hierarchy_item(item): + return FolderItem( + item["id"], + item["parentId"], + item["name"], + item["label"], + None, + None, + ) + + +class CacheItem: + def __init__(self, lifetime=120): + self._lifetime = lifetime + self._last_update = None + self._data = None + + @property + def is_valid(self): + if self._last_update is None: + return False + + return (time.time() - self._last_update) < self._lifetime + + def set_invalid(self, data=None): + self._last_update = None + self._data = data + + def get_data(self): + return self._data + + def update_data(self, data): + self._data = data + self._last_update = time.time() + + +class EntitiesModel(object): + event_source = "entities.model" + + def __init__(self, controller): + folders_cache = CacheItem() + folders_cache.set_invalid({}) + self._folders_cache = folders_cache + self._tasks_cache = {} + + self._folders_by_id = {} + self._tasks_by_id = {} + + self._folders_refreshing = False + self._tasks_refreshing = set() + self._controller = controller + + def reset(self): + self._folders_cache.set_invalid({}) + self._tasks_cache = {} + + self._folders_by_id = {} + self._tasks_by_id = {} + + def refresh(self): + self._refresh_folders_cache() + + def get_folder_items(self, sender): + if not self._folders_cache.is_valid: + self._refresh_folders_cache(sender) + return self._folders_cache.get_data() + + def get_tasks_items(self, folder_id, sender): + if not folder_id: + return [] + + task_cache = self._tasks_cache.get(folder_id) + if task_cache is None or not task_cache.is_valid: + self._refresh_tasks_cache(folder_id, sender) + task_cache = self._tasks_cache.get(folder_id) + return task_cache.get_data() + + def get_folder_entity(self, folder_id): + if folder_id not in self._folders_by_id: + entity = None + if folder_id: + project_name = self._controller.get_current_project_name() + entity = ayon_api.get_folder_by_id(project_name, folder_id) + self._folders_by_id[folder_id] = entity + return self._folders_by_id[folder_id] + + def get_task_entity(self, task_id): + if task_id not in self._tasks_by_id: + entity = None + if task_id: + project_name = self._controller.get_current_project_name() + entity = ayon_api.get_task_by_id(project_name, task_id) + self._tasks_by_id[task_id] = entity + return self._tasks_by_id[task_id] + + @contextlib.contextmanager + def _folder_refresh_event_manager(self, project_name, sender): + self._folders_refreshing = True + self._controller.emit_event( + "folders.refresh.started", + {"project_name": project_name, "sender": sender}, + self.event_source + ) + try: + yield + + finally: + self._controller.emit_event( + "folders.refresh.finished", + {"project_name": project_name, "sender": sender}, + self.event_source + ) + self._folders_refreshing = False + + @contextlib.contextmanager + def _task_refresh_event_manager( + self, project_name, folder_id, sender + ): + self._tasks_refreshing.add(folder_id) + self._controller.emit_event( + "tasks.refresh.started", + { + "project_name": project_name, + "folder_id": folder_id, + "sender": sender, + }, + self.event_source + ) + try: + yield + + finally: + self._controller.emit_event( + "tasks.refresh.finished", + { + "project_name": project_name, + "folder_id": folder_id, + "sender": sender, + }, + self.event_source + ) + self._tasks_refreshing.discard(folder_id) + + def _refresh_folders_cache(self, sender=None): + if self._folders_refreshing: + return + project_name = self._controller.get_current_project_name() + with self._folder_refresh_event_manager(project_name, sender): + folder_items = self._query_folders(project_name) + self._folders_cache.update_data(folder_items) + + def _query_folders(self, project_name): + hierarchy = ayon_api.get_folders_hierarchy(project_name) + + folder_items = {} + hierachy_queue = collections.deque(hierarchy["hierarchy"]) + while hierachy_queue: + item = hierachy_queue.popleft() + folder_item = _get_folder_item_from_hierarchy_item(item) + folder_items[folder_item.entity_id] = folder_item + hierachy_queue.extend(item["children"] or []) + return folder_items + + def _refresh_tasks_cache(self, folder_id, sender=None): + if folder_id in self._tasks_refreshing: + return + + project_name = self._controller.get_current_project_name() + with self._task_refresh_event_manager( + project_name, folder_id, sender + ): + cache_item = self._tasks_cache.get(folder_id) + if cache_item is None: + cache_item = CacheItem() + self._tasks_cache[folder_id] = cache_item + + task_items = self._query_tasks(project_name, folder_id) + cache_item.update_data(task_items) + + def _query_tasks(self, project_name, folder_id): + tasks = list(ayon_api.get_tasks( + project_name, + folder_ids=[folder_id], + fields={"id", "name", "label", "folderId", "type"} + )) + return _get_task_items_from_tasks(tasks) diff --git a/openpype/tools/ayon_workfiles/models/selection.py b/openpype/tools/ayon_workfiles/models/selection.py new file mode 100644 index 0000000000..ad034794d8 --- /dev/null +++ b/openpype/tools/ayon_workfiles/models/selection.py @@ -0,0 +1,91 @@ +class SelectionModel(object): + """Model handling selection changes. + + Triggering events: + - "selection.folder.changed" + - "selection.task.changed" + - "workarea.selection.changed" + - "selection.representation.changed" + """ + + event_source = "selection.model" + + def __init__(self, controller): + self._controller = controller + + self._folder_id = None + self._task_name = None + self._task_id = None + self._workfile_path = None + self._representation_id = None + + def get_selected_folder_id(self): + return self._folder_id + + def set_selected_folder(self, folder_id): + if folder_id == self._folder_id: + return + + self._folder_id = folder_id + self._controller.emit_event( + "selection.folder.changed", + {"folder_id": folder_id}, + self.event_source + ) + + def get_selected_task_name(self): + return self._task_name + + def get_selected_task_id(self): + return self._task_id + + def set_selected_task(self, folder_id, task_id, task_name): + if folder_id != self._folder_id: + self.set_selected_folder(folder_id) + + if task_id == self._task_id: + return + + self._task_name = task_name + self._task_id = task_id + self._controller.emit_event( + "selection.task.changed", + { + "folder_id": folder_id, + "task_name": task_name, + "task_id": task_id + }, + self.event_source + ) + + def get_selected_workfile_path(self): + return self._workfile_path + + def set_selected_workfile_path(self, path): + if path == self._workfile_path: + return + + self._workfile_path = path + self._controller.emit_event( + "workarea.selection.changed", + { + "path": path, + "folder_id": self._folder_id, + "task_name": self._task_name, + "task_id": self._task_id, + }, + self.event_source + ) + + def get_selected_representation_id(self): + return self._representation_id + + def set_selected_representation_id(self, representation_id): + if representation_id == self._representation_id: + return + self._representation_id = representation_id + self._controller.emit_event( + "selection.representation.changed", + {"representation_id": representation_id}, + self.event_source + ) diff --git a/openpype/tools/ayon_workfiles/models/workfiles.py b/openpype/tools/ayon_workfiles/models/workfiles.py new file mode 100644 index 0000000000..eb82f62de3 --- /dev/null +++ b/openpype/tools/ayon_workfiles/models/workfiles.py @@ -0,0 +1,711 @@ +import os +import re +import copy + +import arrow +import ayon_api +from ayon_api.operations import OperationsSession + +from openpype.client import get_project +from openpype.client.operations import ( + prepare_workfile_info_update_data, +) +from openpype.pipeline.template_data import ( + get_template_data, +) +from openpype.pipeline.workfile import ( + get_workdir_with_workdir_data, + get_workfile_template_key, + get_last_workfile_with_version, +) +from openpype.pipeline.version_start import get_versioning_start +from openpype.tools.ayon_workfiles.abstract import ( + WorkareaFilepathResult, + FileItem, + WorkfileInfo, +) + + +def get_folder_template_data(folder): + if not folder: + return {} + parts = folder["path"].split("/") + parts.pop(-1) + hierarchy = "/".join(parts) + return { + "asset": folder["name"], + "folder": { + "name": folder["name"], + "type": folder["folderType"], + "path": folder["path"], + }, + "hierarchy": hierarchy, + } + + +def get_task_template_data(task): + if not task: + return {} + return { + "task": { + "name": task["name"], + "type": task["taskType"] + } + } + + +class CommentMatcher(object): + """Use anatomy and work file data to parse comments from filenames""" + def __init__(self, extensions, file_template, data): + self.fname_regex = None + + if "{comment}" not in file_template: + # Don't look for comment if template doesn't allow it + return + + # Create a regex group for extensions + any_extension = "(?:{})".format( + "|".join(re.escape(ext.lstrip(".")) for ext in extensions) + ) + + # Use placeholders that will never be in the filename + temp_data = copy.deepcopy(data) + temp_data["comment"] = "<>" + temp_data["version"] = "<>" + temp_data["ext"] = "<>" + + fname_pattern = file_template.format_strict(temp_data) + fname_pattern = re.escape(fname_pattern) + + # Replace comment and version with something we can match with regex + replacements = { + "<>": "(.+)", + "<>": "[0-9]+", + "<>": any_extension, + } + for src, dest in replacements.items(): + fname_pattern = fname_pattern.replace(re.escape(src), dest) + + # Match from beginning to end of string to be safe + fname_pattern = "^{}$".format(fname_pattern) + + self.fname_regex = re.compile(fname_pattern) + + def parse_comment(self, filepath): + """Parse the {comment} part from a filename""" + if not self.fname_regex: + return + + fname = os.path.basename(filepath) + match = self.fname_regex.match(fname) + if match: + return match.group(1) + + +class WorkareaModel: + """Workfiles model looking for workfiles in workare folder. + + Workarea folder is usually task and host specific, defined by + anatomy templates. Is looking for files with extensions defined + by host integration. + """ + + def __init__(self, controller): + self._controller = controller + extensions = None + if controller.is_host_valid(): + extensions = controller.get_workfile_extensions() + self._extensions = extensions + self._base_data = None + self._fill_data_by_folder_id = {} + self._task_data_by_folder_id = {} + self._workdir_by_context = {} + + @property + def project_name(self): + return self._controller.get_current_project_name() + + def reset(self): + self._base_data = None + self._fill_data_by_folder_id = {} + self._task_data_by_folder_id = {} + + def _get_base_data(self): + if self._base_data is None: + base_data = get_template_data(get_project(self.project_name)) + base_data["app"] = self._controller.get_host_name() + self._base_data = base_data + return copy.deepcopy(self._base_data) + + def _get_folder_data(self, folder_id): + fill_data = self._fill_data_by_folder_id.get(folder_id) + if fill_data is None: + folder = self._controller.get_folder_entity(folder_id) + fill_data = get_folder_template_data(folder) + self._fill_data_by_folder_id[folder_id] = fill_data + return copy.deepcopy(fill_data) + + def _get_task_data(self, folder_id, task_id): + task_data = self._task_data_by_folder_id.setdefault(folder_id, {}) + if task_id not in task_data: + task = self._controller.get_task_entity(task_id) + if task: + task_data[task_id] = get_task_template_data(task) + return copy.deepcopy(task_data[task_id]) + + def _prepare_fill_data(self, folder_id, task_id): + if not folder_id or not task_id: + return {} + + base_data = self._get_base_data() + folder_data = self._get_folder_data(folder_id) + task_data = self._get_task_data(folder_id, task_id) + + base_data.update(folder_data) + base_data.update(task_data) + + return base_data + + def get_workarea_dir_by_context(self, folder_id, task_id): + if not folder_id or not task_id: + return None + folder_mapping = self._workdir_by_context.setdefault(folder_id, {}) + workdir = folder_mapping.get(task_id) + if workdir is not None: + return workdir + + workdir_data = self._prepare_fill_data(folder_id, task_id) + + workdir = get_workdir_with_workdir_data( + workdir_data, + self.project_name, + anatomy=self._controller.project_anatomy, + ) + folder_mapping[task_id] = workdir + return workdir + + def get_file_items(self, folder_id, task_id): + items = [] + if not folder_id or not task_id: + return items + + workdir = self.get_workarea_dir_by_context(folder_id, task_id) + if not os.path.exists(workdir): + return items + + for filename in os.listdir(workdir): + filepath = os.path.join(workdir, filename) + if not os.path.isfile(filepath): + continue + + ext = os.path.splitext(filename)[1].lower() + if ext not in self._extensions: + continue + + modified = os.path.getmtime(filepath) + items.append( + FileItem(workdir, filename, modified) + ) + return items + + def _get_template_key(self, fill_data): + task_type = fill_data.get("task", {}).get("type") + # TODO cache + return get_workfile_template_key( + task_type, + self._controller.get_host_name(), + project_name=self.project_name + ) + + def _get_last_workfile_version( + self, workdir, file_template, fill_data, extensions + ): + version = get_last_workfile_with_version( + workdir, str(file_template), fill_data, extensions + )[1] + + if version is None: + task_info = fill_data.get("task", {}) + version = get_versioning_start( + self.project_name, + self._controller.get_host_name(), + task_name=task_info.get("name"), + task_type=task_info.get("type"), + family="workfile", + project_settings=self._controller.project_settings, + ) + else: + version += 1 + return version + + def _get_comments_from_root( + self, + file_template, + extensions, + fill_data, + root, + current_filename, + ): + current_comment = None + comment_hints = set() + filenames = [] + if root and os.path.exists(root): + for filename in os.listdir(root): + path = os.path.join(root, filename) + if not os.path.isfile(path): + continue + + ext = os.path.splitext(filename)[-1].lower() + if ext in extensions: + filenames.append(filename) + + if not filenames: + return comment_hints, current_comment + + matcher = CommentMatcher(extensions, file_template, fill_data) + + for filename in filenames: + comment = matcher.parse_comment(filename) + if comment: + comment_hints.add(comment) + if filename == current_filename: + current_comment = comment + + return list(comment_hints), current_comment + + def _get_workdir(self, anatomy, template_key, fill_data): + template_info = anatomy.templates_obj[template_key] + directory_template = template_info["folder"] + return directory_template.format_strict(fill_data).normalized() + + def get_workarea_save_as_data(self, folder_id, task_id): + folder = None + task = None + if folder_id: + folder = self._controller.get_folder_entity(folder_id) + if task_id: + task = self._controller.get_task_entity(task_id) + + if not folder or not task: + return { + "template_key": None, + "template_has_version": None, + "template_has_comment": None, + "ext": None, + "workdir": None, + "comment": None, + "comment_hints": None, + "last_version": None, + "extensions": None, + } + + anatomy = self._controller.project_anatomy + fill_data = self._prepare_fill_data(folder_id, task_id) + template_key = self._get_template_key(fill_data) + + current_workfile = self._controller.get_current_workfile() + current_filename = None + current_ext = None + if current_workfile: + current_filename = os.path.basename(current_workfile) + current_ext = os.path.splitext(current_filename)[1].lower() + + extensions = self._extensions + if not current_ext and extensions: + current_ext = tuple(extensions)[0] + + workdir = self._get_workdir(anatomy, template_key, fill_data) + + template_info = anatomy.templates_obj[template_key] + file_template = template_info["file"] + + comment_hints, comment = self._get_comments_from_root( + file_template, + extensions, + fill_data, + workdir, + current_filename, + ) + last_version = self._get_last_workfile_version( + workdir, file_template, fill_data, extensions) + str_file_template = str(file_template) + template_has_version = "{version" in str_file_template + template_has_comment = "{comment" in str_file_template + + return { + "template_key": template_key, + "template_has_version": template_has_version, + "template_has_comment": template_has_comment, + "ext": current_ext, + "workdir": workdir, + "comment": comment, + "comment_hints": comment_hints, + "last_version": last_version, + "extensions": extensions, + } + + def fill_workarea_filepath( + self, + folder_id, + task_id, + extension, + use_last_version, + version, + comment, + ): + anatomy = self._controller.project_anatomy + fill_data = self._prepare_fill_data(folder_id, task_id) + template_key = self._get_template_key(fill_data) + + workdir = self._get_workdir(anatomy, template_key, fill_data) + + template_info = anatomy.templates_obj[template_key] + file_template = template_info["file"] + + if use_last_version: + version = self._get_last_workfile_version( + workdir, file_template, fill_data, self._extensions + ) + fill_data["version"] = version + fill_data["ext"] = extension.lstrip(".") + + if comment: + fill_data["comment"] = comment + + filename = file_template.format(fill_data) + if not filename.solved: + filename = None + + exists = False + if filename: + filepath = os.path.join(workdir, filename) + exists = os.path.exists(filepath) + + return WorkareaFilepathResult( + workdir, + filename, + exists + ) + + +class WorkfileEntitiesModel: + """Workfile entities model. + + Args: + control (AbstractWorkfileController): Controller object. + """ + + def __init__(self, controller): + self._controller = controller + self._cache = {} + self._items = {} + + def _get_workfile_info_identifier( + self, folder_id, task_id, rootless_path + ): + return "_".join([folder_id, task_id, rootless_path]) + + def _get_rootless_path(self, filepath): + anatomy = self._controller.project_anatomy + + workdir, filename = os.path.split(filepath) + success, rootless_dir = anatomy.find_root_template_from_path(workdir) + return "/".join([ + os.path.normpath(rootless_dir).replace("\\", "/"), + filename + ]) + + def _prepare_workfile_info_item( + self, folder_id, task_id, workfile_info, filepath + ): + note = "" + if workfile_info: + note = workfile_info["attrib"].get("description") or "" + + filestat = os.stat(filepath) + return WorkfileInfo( + folder_id, + task_id, + filepath, + filesize=filestat.st_size, + creation_time=filestat.st_ctime, + modification_time=filestat.st_mtime, + note=note + ) + + def _get_workfile_info(self, folder_id, task_id, identifier): + workfile_info = self._cache.get(identifier) + if workfile_info is not None: + return workfile_info + + for workfile_info in ayon_api.get_workfiles_info( + self._controller.get_current_project_name(), + task_ids=[task_id], + fields=["id", "path", "attrib"], + ): + workfile_identifier = self._get_workfile_info_identifier( + folder_id, task_id, workfile_info["path"] + ) + self._cache[workfile_identifier] = workfile_info + return self._cache.get(identifier) + + def get_workfile_info( + self, folder_id, task_id, filepath, rootless_path=None + ): + if not folder_id or not task_id or not filepath: + return None + + if rootless_path is None: + rootless_path = self._get_rootless_path(filepath) + + identifier = self._get_workfile_info_identifier( + folder_id, task_id, rootless_path) + item = self._items.get(identifier) + if item is None: + workfile_info = self._get_workfile_info( + folder_id, task_id, identifier + ) + item = self._prepare_workfile_info_item( + folder_id, task_id, workfile_info, filepath + ) + self._items[identifier] = item + return item + + def save_workfile_info(self, folder_id, task_id, filepath, note): + rootless_path = self._get_rootless_path(filepath) + identifier = self._get_workfile_info_identifier( + folder_id, task_id, rootless_path + ) + workfile_info = self._get_workfile_info( + folder_id, task_id, identifier + ) + if not workfile_info: + self._cache[identifier] = self._create_workfile_info_entity( + task_id, rootless_path, note) + self._items.pop(identifier, None) + return + + new_workfile_info = copy.deepcopy(workfile_info) + attrib = new_workfile_info.setdefault("attrib", {}) + attrib["description"] = note + update_data = prepare_workfile_info_update_data( + workfile_info, new_workfile_info + ) + self._cache[identifier] = new_workfile_info + self._items.pop(identifier, None) + if not update_data: + return + + project_name = self._controller.get_current_project_name() + + session = OperationsSession() + session.update_entity( + project_name, "workfile", workfile_info["id"], update_data + ) + session.commit() + + def _create_workfile_info_entity(self, task_id, rootless_path, note): + extension = os.path.splitext(rootless_path)[1] + + project_name = self._controller.get_current_project_name() + + workfile_info = { + "path": rootless_path, + "taskId": task_id, + "attrib": { + "extension": extension, + "description": note + } + } + + session = OperationsSession() + session.create_entity(project_name, "workfile", workfile_info) + session.commit() + return workfile_info + + +class PublishWorkfilesModel: + """Model for handling of published workfiles. + + Todos: + Cache workfiles products and representations for some time. + Note Representations won't change. Only what can change are + versions. + """ + + def __init__(self, controller): + self._controller = controller + self._cached_extensions = None + self._cached_repre_extensions = None + + @property + def _extensions(self): + if self._cached_extensions is None: + exts = self._controller.get_workfile_extensions() or [] + self._cached_extensions = exts + return self._cached_extensions + + @property + def _repre_extensions(self): + if self._cached_repre_extensions is None: + self._cached_repre_extensions = { + ext.lstrip(".") for ext in self._extensions + } + return self._cached_repre_extensions + + def _file_item_from_representation( + self, repre_entity, project_anatomy, task_name=None + ): + if task_name is not None: + task_info = repre_entity["context"].get("task") + if not task_info or task_info["name"] != task_name: + return None + + # Filter by extension + extensions = self._repre_extensions + workfile_path = None + for repre_file in repre_entity["files"]: + ext = ( + os.path.splitext(repre_file["name"])[1] + .lower() + .lstrip(".") + ) + if ext in extensions: + workfile_path = repre_file["path"] + break + + if not workfile_path: + return None + + try: + workfile_path = workfile_path.format( + root=project_anatomy.roots) + except Exception as exc: + print("Failed to format workfile path: {}".format(exc)) + + dirpath, filename = os.path.split(workfile_path) + created_at = arrow.get(repre_entity["createdAt"]) + return FileItem( + dirpath, + filename, + created_at.float_timestamp, + repre_entity["id"] + ) + + def get_file_items(self, folder_id, task_name): + # TODO refactor to use less server API calls + project_name = self._controller.get_current_project_name() + # Get subset docs of asset + product_entities = ayon_api.get_products( + project_name, + folder_ids=[folder_id], + product_types=["workfile"], + fields=["id", "name"] + ) + + output = [] + product_ids = {product["id"] for product in product_entities} + if not product_ids: + return output + + # Get version docs of subsets with their families + version_entities = ayon_api.get_versions( + project_name, + product_ids=product_ids, + fields=["id", "productId"] + ) + version_ids = {version["id"] for version in version_entities} + if not version_ids: + return output + + # Query representations of filtered versions and add filter for + # extension + repre_entities = ayon_api.get_representations( + project_name, + version_ids=version_ids + ) + project_anatomy = self._controller.project_anatomy + + # Filter queried representations by task name if task is set + file_items = [] + for repre_entity in repre_entities: + file_item = self._file_item_from_representation( + repre_entity, project_anatomy, task_name + ) + if file_item is not None: + file_items.append(file_item) + + return file_items + + +class WorkfilesModel: + """Workfiles model.""" + + def __init__(self, controller): + self._controller = controller + + self._entities_model = WorkfileEntitiesModel(controller) + self._workarea_model = WorkareaModel(controller) + self._published_model = PublishWorkfilesModel(controller) + + def get_workfile_info(self, folder_id, task_id, filepath): + return self._entities_model.get_workfile_info( + folder_id, task_id, filepath + ) + + def save_workfile_info(self, folder_id, task_id, filepath, note): + self._entities_model.save_workfile_info( + folder_id, task_id, filepath, note + ) + + def get_workarea_dir_by_context(self, folder_id, task_id): + """Workarea dir for passed context. + + The directory path is based on project anatomy templates. + + Args: + folder_id (str): Folder id. + task_id (str): Task id. + + Returns: + Union[str, None]: Workarea dir path or None for invalid context. + """ + + return self._workarea_model.get_workarea_dir_by_context( + folder_id, task_id) + + def get_workarea_file_items(self, folder_id, task_id): + """Workfile items for passed context from workarea. + + Args: + folder_id (Union[str, None]): Folder id. + task_id (Union[str, None]): Task id. + + Returns: + list[FileItem]: List of file items matching workarea of passed + context. + """ + + return self._workarea_model.get_file_items(folder_id, task_id) + + def get_workarea_save_as_data(self, folder_id, task_id): + return self._workarea_model.get_workarea_save_as_data( + folder_id, task_id) + + def fill_workarea_filepath(self, *args, **kwargs): + return self._workarea_model.fill_workarea_filepath( + *args, **kwargs + ) + + def get_published_file_items(self, folder_id, task_name): + """Published workfiles for passed context. + + Args: + folder_id (str): Folder id. + task_name (str): Task name. + + Returns: + list[FileItem]: List of files for published workfiles. + """ + + return self._published_model.get_file_items(folder_id, task_name) diff --git a/openpype/tools/ayon_workfiles/widgets/__init__.py b/openpype/tools/ayon_workfiles/widgets/__init__.py new file mode 100644 index 0000000000..f0c5ba1c40 --- /dev/null +++ b/openpype/tools/ayon_workfiles/widgets/__init__.py @@ -0,0 +1,6 @@ +from .window import WorkfilesToolWindow + + +__all__ = ( + "WorkfilesToolWindow", +) diff --git a/openpype/tools/ayon_workfiles/widgets/constants.py b/openpype/tools/ayon_workfiles/widgets/constants.py new file mode 100644 index 0000000000..fc74fd9bc4 --- /dev/null +++ b/openpype/tools/ayon_workfiles/widgets/constants.py @@ -0,0 +1,7 @@ +from qtpy import QtCore + + +ITEM_ID_ROLE = QtCore.Qt.UserRole + 1 +PARENT_ID_ROLE = QtCore.Qt.UserRole + 2 +ITEM_NAME_ROLE = QtCore.Qt.UserRole + 3 +TASK_TYPE_ROLE = QtCore.Qt.UserRole + 4 diff --git a/openpype/tools/ayon_workfiles/widgets/files_widget.py b/openpype/tools/ayon_workfiles/widgets/files_widget.py new file mode 100644 index 0000000000..fbf4dbc593 --- /dev/null +++ b/openpype/tools/ayon_workfiles/widgets/files_widget.py @@ -0,0 +1,398 @@ +import os + +import qtpy +from qtpy import QtWidgets, QtCore + +from .save_as_dialog import SaveAsDialog +from .files_widget_workarea import WorkAreaFilesWidget +from .files_widget_published import PublishedFilesWidget + + +class FilesWidget(QtWidgets.QWidget): + """A widget displaying files that allows to save and open files. + + Args: + controller (AbstractWorkfilesFrontend): The control object. + parent (QtWidgets.QWidget): The parent widget. + """ + + def __init__(self, controller, parent): + super(FilesWidget, self).__init__(parent) + + files_widget = QtWidgets.QStackedWidget(self) + workarea_widget = WorkAreaFilesWidget(controller, files_widget) + published_widget = PublishedFilesWidget(controller, files_widget) + files_widget.addWidget(workarea_widget) + files_widget.addWidget(published_widget) + + btns_widget = QtWidgets.QWidget(self) + + workarea_btns_widget = QtWidgets.QWidget(btns_widget) + workarea_btn_open = QtWidgets.QPushButton( + "Open", workarea_btns_widget) + workarea_btn_browse = QtWidgets.QPushButton( + "Browse", workarea_btns_widget) + workarea_btn_save = QtWidgets.QPushButton( + "Save As", workarea_btns_widget) + + workarea_btns_layout = QtWidgets.QHBoxLayout(workarea_btns_widget) + workarea_btns_layout.setContentsMargins(0, 0, 0, 0) + workarea_btns_layout.addWidget(workarea_btn_open, 1) + workarea_btns_layout.addWidget(workarea_btn_browse, 1) + workarea_btns_layout.addWidget(workarea_btn_save, 1) + + published_btns_widget = QtWidgets.QWidget(btns_widget) + published_btn_copy_n_open = QtWidgets.QPushButton( + "Copy && Open", published_btns_widget + ) + published_btn_change_context = QtWidgets.QPushButton( + "Choose different context", published_btns_widget + ) + published_btn_cancel = QtWidgets.QPushButton( + "Cancel", published_btns_widget + ) + + published_btns_layout = QtWidgets.QHBoxLayout(published_btns_widget) + published_btns_layout.setContentsMargins(0, 0, 0, 0) + published_btns_layout.addWidget(published_btn_copy_n_open, 1) + published_btns_layout.addWidget(published_btn_change_context, 1) + published_btns_layout.addWidget(published_btn_cancel, 1) + + btns_layout = QtWidgets.QVBoxLayout(btns_widget) + btns_layout.setContentsMargins(0, 0, 0, 0) + btns_layout.addWidget(workarea_btns_widget, 1) + btns_layout.addWidget(published_btns_widget, 1) + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(files_widget, 1) + main_layout.addWidget(btns_widget, 0) + + controller.register_event_callback( + "workarea.selection.changed", + self._on_workarea_path_changed + ) + controller.register_event_callback( + "selection.representation.changed", + self._on_published_repre_changed + ) + controller.register_event_callback( + "selection.task.changed", + self._on_task_changed + ) + controller.register_event_callback( + "copy_representation.finished", + self._on_copy_representation_finished, + ) + controller.register_event_callback( + "workfile_save_enable.changed", + self._on_workfile_save_enabled_change, + ) + + workarea_widget.open_current_requested.connect( + self._on_current_open_requests) + workarea_widget.duplicate_requested.connect( + self._on_duplicate_request) + workarea_btn_open.clicked.connect(self._on_workarea_open_clicked) + workarea_btn_browse.clicked.connect(self._on_workarea_browse_clicked) + workarea_btn_save.clicked.connect(self._on_workarea_save_clicked) + + published_widget.save_as_requested.connect(self._on_save_as_request) + published_btn_copy_n_open.clicked.connect( + self._on_published_save_clicked) + published_btn_change_context.clicked.connect( + self._on_published_change_context_clicked) + published_btn_cancel.clicked.connect( + self._on_published_cancel_clicked) + + self._selected_folder_id = None + self._selected_tak_name = None + + self._pre_select_folder_id = None + self._pre_select_task_name = None + + self._select_context_mode = False + self._valid_selected_context = False + self._valid_representation_id = False + self._tmp_text_filter = None + self._is_save_enabled = True + + self._controller = controller + self._files_widget = files_widget + self._workarea_widget = workarea_widget + self._published_widget = published_widget + self._workarea_btns_widget = workarea_btns_widget + self._published_btns_widget = published_btns_widget + + self._workarea_btn_open = workarea_btn_open + self._workarea_btn_browse = workarea_btn_browse + self._workarea_btn_save = workarea_btn_save + + self._published_widget = published_widget + self._published_btn_copy_n_open = published_btn_copy_n_open + self._published_btn_change_context = published_btn_change_context + self._published_btn_cancel = published_btn_cancel + + # Initial setup + workarea_btn_open.setEnabled(False) + published_btn_copy_n_open.setEnabled(False) + published_btn_change_context.setEnabled(False) + published_btn_cancel.setVisible(False) + + def set_published_mode(self, published_mode): + # Make sure context selection is disabled + self._set_select_contex_mode(False) + # Change current widget + self._files_widget.setCurrentWidget(( + self._published_widget + if published_mode + else self._workarea_widget + )) + # Pass the mode to the widgets, so they can start/stop handle events + self._workarea_widget.set_published_mode(published_mode) + self._published_widget.set_published_mode(published_mode) + + # Change available buttons + self._workarea_btns_widget.setVisible(not published_mode) + self._published_btns_widget.setVisible(published_mode) + + def set_text_filter(self, text_filter): + if self._select_context_mode: + self._tmp_text_filter = text_filter + return + self._workarea_widget.set_text_filter(text_filter) + self._published_widget.set_text_filter(text_filter) + + def _exec_save_as_dialog(self): + """Show SaveAs dialog using currently selected context. + + Returns: + Union[dict[str, Any], None]: Result of the dialog. + """ + + dialog = SaveAsDialog(self._controller, self) + dialog.update_context() + dialog.exec_() + return dialog.get_result() + + # ------------------------------------------------------------- + # Workarea workfiles + # ------------------------------------------------------------- + def _open_workfile(self, filepath): + if self._controller.has_unsaved_changes(): + result = self._save_changes_prompt() + if result is None: + return + + if result: + self._controller.save_current_workfile() + self._controller.open_workfile(filepath) + + def _on_workarea_open_clicked(self): + path = self._workarea_widget.get_selected_path() + if path: + self._open_workfile(path) + + def _on_current_open_requests(self): + self._on_workarea_open_clicked() + + def _on_duplicate_request(self): + filepath = self._workarea_widget.get_selected_path() + if filepath is None: + return + + result = self._exec_save_as_dialog() + if result is None: + return + self._controller.duplicate_workfile( + filepath, + result["workdir"], + result["filename"] + ) + + def _on_workarea_browse_clicked(self): + extnsions = self._controller.get_workfile_extensions() + ext_filter = "Work File (*{0})".format( + " *".join(extnsions) + ) + dir_key = "directory" + if qtpy.API in ("pyside", "pyside2", "pyside6"): + dir_key = "dir" + + selected_context = self._controller.get_selected_context() + workfile_root = self._controller.get_workarea_dir_by_context( + selected_context["folder_id"], selected_context["task_id"] + ) + # Find existing directory of workfile root + # - Qt will use 'cwd' instead, if path does not exist, which may lead + # to igniter directory + while workfile_root: + if os.path.exists(workfile_root): + break + workfile_root = os.path.dirname(workfile_root) + + kwargs = { + "caption": "Work Files", + "filter": ext_filter, + dir_key: workfile_root + } + + filepath = QtWidgets.QFileDialog.getOpenFileName(**kwargs)[0] + if filepath: + self._open_workfile(filepath) + + def _on_workarea_save_clicked(self): + result = self._exec_save_as_dialog() + if result is None: + return + self._controller.save_as_workfile( + result["folder_id"], + result["task_id"], + result["workdir"], + result["filename"], + result["template_key"], + ) + + def _on_workarea_path_changed(self, event): + valid_path = event["path"] is not None + self._workarea_btn_open.setEnabled(valid_path) + + # ------------------------------------------------------------- + # Published workfiles + # ------------------------------------------------------------- + def _update_published_btns_state(self): + enabled = ( + self._valid_representation_id + and self._valid_selected_context + and self._is_save_enabled + ) + self._published_btn_copy_n_open.setEnabled(enabled) + self._published_btn_change_context.setEnabled(enabled) + + def _update_workarea_btns_state(self): + enabled = self._is_save_enabled + self._workarea_btn_save.setEnabled(enabled) + + def _on_published_repre_changed(self, event): + self._valid_representation_id = event["representation_id"] is not None + self._update_published_btns_state() + + def _on_task_changed(self, event): + self._selected_folder_id = event["folder_id"] + self._selected_tak_name = event["task_name"] + self._valid_selected_context = ( + self._selected_folder_id is not None + and self._selected_tak_name is not None + ) + self._update_published_btns_state() + + def _on_published_save_clicked(self): + result = self._exec_save_as_dialog() + if result is None: + return + + repre_info = self._published_widget.get_selected_repre_info() + self._controller.copy_workfile_representation( + repre_info["representation_id"], + repre_info["filepath"], + result["folder_id"], + result["task_id"], + result["workdir"], + result["filename"], + result["template_key"], + ) + + def _on_save_as_request(self): + self._on_published_save_clicked() + + def _set_select_contex_mode(self, enabled): + if self._select_context_mode is enabled: + return + + if enabled: + self._pre_select_folder_id = self._selected_folder_id + self._pre_select_task_name = self._selected_tak_name + else: + self._pre_select_folder_id = None + self._pre_select_task_name = None + self._select_context_mode = enabled + self._published_btn_cancel.setVisible(enabled) + self._published_btn_change_context.setVisible(not enabled) + self._published_widget.set_select_context_mode(enabled) + + if not enabled and self._tmp_text_filter is not None: + self.set_text_filter(self._tmp_text_filter) + self._tmp_text_filter = None + + def _on_published_change_context_clicked(self): + self._set_select_contex_mode(True) + + def _should_set_pre_select_context(self): + if self._pre_select_folder_id is None: + return False + if self._pre_select_folder_id != self._selected_folder_id: + return True + if self._pre_select_task_name is None: + return False + return self._pre_select_task_name != self._selected_tak_name + + def _on_published_cancel_clicked(self): + folder_id = self._pre_select_folder_id + task_name = self._pre_select_task_name + representation_id = self._published_widget.get_selected_repre_id() + should_change_selection = self._should_set_pre_select_context() + self._set_select_contex_mode(False) + if should_change_selection: + self._controller.set_expected_selection( + folder_id, task_name, representation_id=representation_id + ) + + def _on_copy_representation_finished(self, event): + """Callback for when copy representation is finished. + + Make sure that select context mode is disabled when representation + copy is finished. + + Args: + event (Event): Event object. + """ + + if not event["failed"]: + self._set_select_contex_mode(False) + + def _on_workfile_save_enabled_change(self, event): + enabled = event["enabled"] + self._is_save_enabled = enabled + self._update_published_btns_state() + self._update_workarea_btns_state() + + def _save_changes_prompt(self): + """Ask user if wants to save changes to current file. + + Returns: + Union[bool, None]: True if user wants to save changes, False if + user does not want to save changes, None if user cancels + operation. + """ + messagebox = QtWidgets.QMessageBox(parent=self) + messagebox.setWindowFlags( + messagebox.windowFlags() | QtCore.Qt.FramelessWindowHint + ) + messagebox.setIcon(QtWidgets.QMessageBox.Warning) + messagebox.setWindowTitle("Unsaved Changes!") + messagebox.setText( + "There are unsaved changes to the current file." + "\nDo you want to save the changes?" + ) + messagebox.setStandardButtons( + QtWidgets.QMessageBox.Yes + | QtWidgets.QMessageBox.No + | QtWidgets.QMessageBox.Cancel + ) + + result = messagebox.exec_() + if result == QtWidgets.QMessageBox.Yes: + return True + if result == QtWidgets.QMessageBox.No: + return False + return None diff --git a/openpype/tools/ayon_workfiles/widgets/files_widget_published.py b/openpype/tools/ayon_workfiles/widgets/files_widget_published.py new file mode 100644 index 0000000000..bc59447777 --- /dev/null +++ b/openpype/tools/ayon_workfiles/widgets/files_widget_published.py @@ -0,0 +1,378 @@ +import qtawesome +from qtpy import QtWidgets, QtCore, QtGui + +from openpype.style import ( + get_default_entity_icon_color, + get_disabled_entity_icon_color, +) +from openpype.tools.utils.delegates import PrettyTimeDelegate + +from .utils import TreeView, BaseOverlayFrame + + +REPRE_ID_ROLE = QtCore.Qt.UserRole + 1 +FILEPATH_ROLE = QtCore.Qt.UserRole + 2 +DATE_MODIFIED_ROLE = QtCore.Qt.UserRole + 3 + + +class PublishedFilesModel(QtGui.QStandardItemModel): + """A model for displaying files. + + Args: + controller (AbstractWorkfilesFrontend): The control object. + """ + + def __init__(self, controller): + super(PublishedFilesModel, self).__init__() + + self.setColumnCount(2) + + self.setHeaderData(0, QtCore.Qt.Horizontal, "Name") + self.setHeaderData(1, QtCore.Qt.Horizontal, "Date Modified") + + controller.register_event_callback( + "selection.task.changed", + self._on_task_changed + ) + controller.register_event_callback( + "selection.folder.changed", + self._on_folder_changed + ) + + self._file_icon = qtawesome.icon( + "fa.file-o", + color=get_default_entity_icon_color() + ) + self._controller = controller + self._items_by_id = {} + self._missing_context_item = None + self._missing_context_used = False + self._empty_root_item = None + self._empty_item_used = False + + self._published_mode = False + self._context_select_mode = False + + self._last_folder_id = None + self._last_task_id = None + + self._add_empty_item() + + def _clear_items(self): + self._remove_missing_context_item() + self._remove_empty_item() + if self._items_by_id: + root = self.invisibleRootItem() + root.removeRows(0, root.rowCount()) + self._items_by_id = {} + + def set_published_mode(self, published_mode): + if self._published_mode == published_mode: + return + self._published_mode = published_mode + if published_mode: + self._fill_items() + elif self._context_select_mode: + self.set_select_context_mode(False) + + def set_select_context_mode(self, select_mode): + if self._context_select_mode is select_mode: + return + self._context_select_mode = select_mode + if not select_mode and self._published_mode: + self._fill_items() + + def get_index_by_representation_id(self, representation_id): + item = self._items_by_id.get(representation_id) + if item is None: + return QtCore.QModelIndex() + return self.indexFromItem(item) + + def _get_missing_context_item(self): + if self._missing_context_item is None: + message = "Select folder" + item = QtGui.QStandardItem(message) + icon = qtawesome.icon( + "fa.times", + color=get_disabled_entity_icon_color() + ) + item.setData(icon, QtCore.Qt.DecorationRole) + item.setFlags(QtCore.Qt.NoItemFlags) + item.setColumnCount(self.columnCount()) + self._missing_context_item = item + return self._missing_context_item + + def _add_missing_context_item(self): + if self._missing_context_used: + return + self._clear_items() + root_item = self.invisibleRootItem() + root_item.appendRow(self._get_missing_context_item()) + self._missing_context_used = True + + def _remove_missing_context_item(self): + if not self._missing_context_used: + return + root_item = self.invisibleRootItem() + root_item.takeRow(self._missing_context_item.row()) + self._missing_context_used = False + + def _get_empty_root_item(self): + if self._empty_root_item is None: + message = "Didn't find any published workfiles." + item = QtGui.QStandardItem(message) + icon = qtawesome.icon( + "fa.times", + color=get_disabled_entity_icon_color() + ) + item.setData(icon, QtCore.Qt.DecorationRole) + item.setFlags(QtCore.Qt.NoItemFlags) + item.setColumnCount(self.columnCount()) + self._empty_root_item = item + return self._empty_root_item + + def _add_empty_item(self): + if self._empty_item_used: + return + self._clear_items() + root_item = self.invisibleRootItem() + root_item.appendRow(self._get_empty_root_item()) + self._empty_item_used = True + + def _remove_empty_item(self): + if not self._empty_item_used: + return + root_item = self.invisibleRootItem() + root_item.takeRow(self._empty_root_item.row()) + self._empty_item_used = False + + def _on_folder_changed(self, event): + self._last_folder_id = event["folder_id"] + self._last_task_id = None + if self._context_select_mode: + return + + if self._published_mode: + self._fill_items() + + def _on_task_changed(self, event): + self._last_folder_id = event["folder_id"] + self._last_task_id = event["task_id"] + if self._context_select_mode: + return + + if self._published_mode: + self._fill_items() + + def _fill_items(self): + folder_id = self._last_folder_id + task_id = self._last_task_id + if not folder_id: + self._add_missing_context_item() + return + + file_items = self._controller.get_published_file_items( + folder_id, task_id + ) + root_item = self.invisibleRootItem() + if not file_items: + self._add_empty_item() + return + self._remove_empty_item() + self._remove_missing_context_item() + + items_to_remove = set(self._items_by_id.keys()) + new_items = [] + for file_item in file_items: + repre_id = file_item.representation_id + if repre_id in self._items_by_id: + items_to_remove.discard(repre_id) + item = self._items_by_id[repre_id] + else: + item = QtGui.QStandardItem() + new_items.append(item) + item.setColumnCount(self.columnCount()) + item.setData(self._file_icon, QtCore.Qt.DecorationRole) + item.setData(file_item.filename, QtCore.Qt.DisplayRole) + item.setData(repre_id, REPRE_ID_ROLE) + + if file_item.exists: + flags = QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable + else: + flags = QtCore.Qt.NoItemFlags + + item.setFlags(flags) + item.setData(file_item.filepath, FILEPATH_ROLE) + item.setData(file_item.modified, DATE_MODIFIED_ROLE) + + self._items_by_id[repre_id] = item + + if new_items: + root_item.appendRows(new_items) + + for repre_id in items_to_remove: + item = self._items_by_id.pop(repre_id) + root_item.removeRow(item.row()) + + if root_item.rowCount() == 0: + self._add_empty_item() + + def flags(self, index): + # Use flags of first column for all columns + if index.column() != 0: + index = self.index(index.row(), 0, index.parent()) + return super(PublishedFilesModel, self).flags(index) + + def data(self, index, role=None): + if role is None: + role = QtCore.Qt.DisplayRole + + # Handle roles for first column + if index.column() == 1: + if role == QtCore.Qt.DecorationRole: + return None + + if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole): + role = DATE_MODIFIED_ROLE + index = self.index(index.row(), 0, index.parent()) + + return super(PublishedFilesModel, self).data(index, role) + + +class SelectContextOverlay(BaseOverlayFrame): + """Overlay for files view when user should select context. + + Todos: + The look of this overlay should be improved, it is "not nice" now. + """ + + def __init__(self, parent): + super(SelectContextOverlay, self).__init__(parent) + + label_widget = QtWidgets.QLabel( + "Please choose context on the left
<", + self + ) + label_widget.setAlignment(QtCore.Qt.AlignCenter) + label_widget.setObjectName("OverlayFrameLabel") + + layout = QtWidgets.QHBoxLayout(self) + layout.addWidget(label_widget, 1, QtCore.Qt.AlignCenter) + + label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + +class PublishedFilesWidget(QtWidgets.QWidget): + """Published workfiles widget. + + Args: + controller (AbstractWorkfilesFrontend): The control object. + parent (QtWidgets.QWidget): The parent widget. + """ + + selection_changed = QtCore.Signal() + save_as_requested = QtCore.Signal() + + def __init__(self, controller, parent): + super(PublishedFilesWidget, self).__init__(parent) + + view = TreeView(self) + view.setSortingEnabled(True) + view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + # Smaller indentation + view.setIndentation(0) + + model = PublishedFilesModel(controller) + proxy_model = QtCore.QSortFilterProxyModel() + proxy_model.setSourceModel(model) + proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive) + proxy_model.setDynamicSortFilter(True) + + view.setModel(proxy_model) + + time_delegate = PrettyTimeDelegate() + view.setItemDelegateForColumn(1, time_delegate) + + # Default to a wider first filename column it is what we mostly care + # about and the date modified is relatively small anyway. + view.setColumnWidth(0, 330) + + select_overlay = SelectContextOverlay(view) + select_overlay.setVisible(False) + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(view, 1) + + selection_model = view.selectionModel() + selection_model.selectionChanged.connect(self._on_selection_change) + view.double_clicked_left.connect(self._on_left_double_click) + + controller.register_event_callback( + "expected_selection_changed", + self._on_expected_selection_change + ) + + self._view = view + self._select_overlay = select_overlay + self._model = model + self._proxy_model = proxy_model + self._time_delegate = time_delegate + self._controller = controller + + def set_published_mode(self, published_mode): + self._model.set_published_mode(published_mode) + + def set_select_context_mode(self, select_mode): + self._model.set_select_context_mode(select_mode) + self._select_overlay.setVisible(select_mode) + + def set_text_filter(self, text_filter): + self._proxy_model.setFilterFixedString(text_filter) + + def get_selected_repre_info(self): + selection_model = self._view.selectionModel() + representation_id = None + filepath = None + for index in selection_model.selectedIndexes(): + representation_id = index.data(REPRE_ID_ROLE) + filepath = index.data(FILEPATH_ROLE) + + return { + "representation_id": representation_id, + "filepath": filepath, + } + + def get_selected_repre_id(self): + return self.get_selected_repre_info()["representation_id"] + + def _on_selection_change(self): + repre_id = self.get_selected_repre_id() + self._controller.set_selected_representation_id(repre_id) + + def _on_left_double_click(self): + self.save_as_requested.emit() + + def _on_expected_selection_change(self, event): + if ( + event["representation_id_selected"] + or not event["folder_selected"] + or (event["task_name"] and not event["task_selected"]) + ): + return + + representation_id = event["representation_id"] + selected_repre_id = self.get_selected_repre_id() + if ( + representation_id is not None + and representation_id != selected_repre_id + ): + index = self._model.get_index_by_representation_id( + representation_id) + if index.isValid(): + proxy_index = self._proxy_model.mapFromSource(index) + self._view.setCurrentIndex(proxy_index) + + self._controller.expected_representation_selected( + event["folder_id"], event["task_name"], representation_id + ) diff --git a/openpype/tools/ayon_workfiles/widgets/files_widget_workarea.py b/openpype/tools/ayon_workfiles/widgets/files_widget_workarea.py new file mode 100644 index 0000000000..e8ccd094d1 --- /dev/null +++ b/openpype/tools/ayon_workfiles/widgets/files_widget_workarea.py @@ -0,0 +1,380 @@ +import qtawesome +from qtpy import QtWidgets, QtCore, QtGui + +from openpype.style import ( + get_default_entity_icon_color, + get_disabled_entity_icon_color, +) +from openpype.tools.utils.delegates import PrettyTimeDelegate + +from .utils import TreeView + +FILENAME_ROLE = QtCore.Qt.UserRole + 1 +FILEPATH_ROLE = QtCore.Qt.UserRole + 2 +DATE_MODIFIED_ROLE = QtCore.Qt.UserRole + 3 + + +class WorkAreaFilesModel(QtGui.QStandardItemModel): + """A model for workare workfiles. + + Args: + controller (AbstractWorkfilesFrontend): The control object. + """ + + def __init__(self, controller): + super(WorkAreaFilesModel, self).__init__() + + self.setColumnCount(2) + + self.setHeaderData(0, QtCore.Qt.Horizontal, "Name") + self.setHeaderData(1, QtCore.Qt.Horizontal, "Date Modified") + + controller.register_event_callback( + "selection.task.changed", + self._on_task_changed + ) + controller.register_event_callback( + "workfile_duplicate.finished", + self._on_duplicate_finished + ) + controller.register_event_callback( + "save_as.finished", + self._on_save_as_finished + ) + + self._file_icon = qtawesome.icon( + "fa.file-o", + color=get_default_entity_icon_color() + ) + self._controller = controller + self._items_by_filename = {} + self._missing_context_item = None + self._missing_context_used = False + self._empty_root_item = None + self._empty_item_used = False + self._published_mode = False + self._selected_folder_id = None + self._selected_task_id = None + + self._add_missing_context_item() + + def get_index_by_filename(self, filename): + item = self._items_by_filename.get(filename) + if item is None: + return QtCore.QModelIndex() + return self.indexFromItem(item) + + def _get_missing_context_item(self): + if self._missing_context_item is None: + message = "Select folder and task" + item = QtGui.QStandardItem(message) + icon = qtawesome.icon( + "fa.times", + color=get_disabled_entity_icon_color() + ) + item.setData(icon, QtCore.Qt.DecorationRole) + item.setFlags(QtCore.Qt.NoItemFlags) + item.setColumnCount(self.columnCount()) + self._missing_context_item = item + return self._missing_context_item + + def _clear_items(self): + self._remove_missing_context_item() + self._remove_empty_item() + if self._items_by_filename: + root = self.invisibleRootItem() + root.removeRows(0, root.rowCount()) + self._items_by_filename = {} + + def _add_missing_context_item(self): + if self._missing_context_used: + return + self._clear_items() + root_item = self.invisibleRootItem() + root_item.appendRow(self._get_missing_context_item()) + self._missing_context_used = True + + def _remove_missing_context_item(self): + if not self._missing_context_used: + return + root_item = self.invisibleRootItem() + root_item.takeRow(self._missing_context_item.row()) + self._missing_context_used = False + + def _get_empty_root_item(self): + if self._empty_root_item is None: + message = "Work Area is empty.." + item = QtGui.QStandardItem(message) + icon = qtawesome.icon( + "fa.exclamation-circle", + color=get_disabled_entity_icon_color() + ) + item.setData(icon, QtCore.Qt.DecorationRole) + item.setFlags(QtCore.Qt.NoItemFlags) + item.setColumnCount(self.columnCount()) + self._empty_root_item = item + return self._empty_root_item + + def _add_empty_item(self): + if self._empty_item_used: + return + self._clear_items() + root_item = self.invisibleRootItem() + root_item.appendRow(self._get_empty_root_item()) + self._empty_item_used = True + + def _remove_empty_item(self): + if not self._empty_item_used: + return + root_item = self.invisibleRootItem() + root_item.takeRow(self._empty_root_item.row()) + self._empty_item_used = False + + def _on_task_changed(self, event): + self._selected_folder_id = event["folder_id"] + self._selected_task_id = event["task_id"] + if not self._published_mode: + self._fill_items() + + def _on_duplicate_finished(self, event): + if event["failed"]: + return + + if not self._published_mode: + self._fill_items() + + def _on_save_as_finished(self, event): + if event["failed"]: + return + + if not self._published_mode: + self._fill_items() + + def _fill_items(self): + folder_id = self._selected_folder_id + task_id = self._selected_task_id + if not folder_id or not task_id: + self._add_missing_context_item() + return + + file_items = self._controller.get_workarea_file_items( + folder_id, task_id + ) + root_item = self.invisibleRootItem() + if not file_items: + self._add_empty_item() + return + self._remove_empty_item() + self._remove_missing_context_item() + + items_to_remove = set(self._items_by_filename.keys()) + new_items = [] + for file_item in file_items: + filename = file_item.filename + if filename in self._items_by_filename: + items_to_remove.discard(filename) + item = self._items_by_filename[filename] + else: + item = QtGui.QStandardItem() + new_items.append(item) + item.setColumnCount(self.columnCount()) + item.setFlags( + QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable + ) + item.setData(self._file_icon, QtCore.Qt.DecorationRole) + item.setData(file_item.filename, QtCore.Qt.DisplayRole) + item.setData(file_item.filename, FILENAME_ROLE) + + item.setData(file_item.filepath, FILEPATH_ROLE) + item.setData(file_item.modified, DATE_MODIFIED_ROLE) + + self._items_by_filename[file_item.filename] = item + + if new_items: + root_item.appendRows(new_items) + + for filename in items_to_remove: + item = self._items_by_filename.pop(filename) + root_item.removeRow(item.row()) + + if root_item.rowCount() == 0: + self._add_empty_item() + + def flags(self, index): + # Use flags of first column for all columns + if index.column() != 0: + index = self.index(index.row(), 0, index.parent()) + return super(WorkAreaFilesModel, self).flags(index) + + def data(self, index, role=None): + if role is None: + role = QtCore.Qt.DisplayRole + + # Handle roles for first column + if index.column() == 1: + if role == QtCore.Qt.DecorationRole: + return None + + if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole): + role = DATE_MODIFIED_ROLE + index = self.index(index.row(), 0, index.parent()) + + return super(WorkAreaFilesModel, self).data(index, role) + + def set_published_mode(self, published_mode): + if self._published_mode == published_mode: + return + self._published_mode = published_mode + if not published_mode: + self._fill_items() + + +class WorkAreaFilesWidget(QtWidgets.QWidget): + """Workarea files widget. + + Args: + controller (AbstractWorkfilesFrontend): The control object. + parent (QtWidgets.QWidget): The parent widget. + """ + + selection_changed = QtCore.Signal() + open_current_requested = QtCore.Signal() + duplicate_requested = QtCore.Signal() + + def __init__(self, controller, parent): + super(WorkAreaFilesWidget, self).__init__(parent) + + view = TreeView(self) + view.setSortingEnabled(True) + view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + # Smaller indentation + view.setIndentation(0) + + model = WorkAreaFilesModel(controller) + proxy_model = QtCore.QSortFilterProxyModel() + proxy_model.setSourceModel(model) + proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive) + proxy_model.setDynamicSortFilter(True) + + view.setModel(proxy_model) + + time_delegate = PrettyTimeDelegate() + view.setItemDelegateForColumn(1, time_delegate) + + # Default to a wider first filename column it is what we mostly care + # about and the date modified is relatively small anyway. + view.setColumnWidth(0, 330) + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(view, 1) + + selection_model = view.selectionModel() + selection_model.selectionChanged.connect(self._on_selection_change) + view.double_clicked_left.connect(self._on_left_double_click) + view.customContextMenuRequested.connect(self._on_context_menu) + + controller.register_event_callback( + "expected_selection_changed", + self._on_expected_selection_change + ) + + self._view = view + self._model = model + self._proxy_model = proxy_model + self._time_delegate = time_delegate + self._controller = controller + + self._published_mode = False + + def set_published_mode(self, published_mode): + """Set the published mode. + + Widget should ignore most of events when in published mode is enabled. + + Args: + published_mode (bool): The published mode. + """ + + self._model.set_published_mode(published_mode) + self._published_mode = published_mode + + def set_text_filter(self, text_filter): + """Set the text filter. + + Args: + text_filter (str): The text filter. + """ + + self._proxy_model.setFilterFixedString(text_filter) + + def _get_selected_info(self): + selection_model = self._view.selectionModel() + filepath = None + filename = None + for index in selection_model.selectedIndexes(): + filepath = index.data(FILEPATH_ROLE) + filename = index.data(FILENAME_ROLE) + return { + "filepath": filepath, + "filename": filename, + } + + def get_selected_path(self): + """Selected filepath. + + Returns: + Union[str, None]: The selected filepath or None if nothing is + selected. + """ + return self._get_selected_info()["filepath"] + + def _on_selection_change(self): + filepath = self.get_selected_path() + self._controller.set_selected_workfile_path(filepath) + + def _on_left_double_click(self): + self.open_current_requested.emit() + + def _on_context_menu(self, point): + index = self._view.indexAt(point) + if not index.isValid(): + return + + if not index.flags() & QtCore.Qt.ItemIsEnabled: + return + + menu = QtWidgets.QMenu(self) + + # Duplicate + action = QtWidgets.QAction("Duplicate", menu) + tip = "Duplicate selected file." + action.setToolTip(tip) + action.setStatusTip(tip) + action.triggered.connect(self._on_duplicate_pressed) + menu.addAction(action) + + # Show the context action menu + global_point = self._view.mapToGlobal(point) + _ = menu.exec_(global_point) + + def _on_duplicate_pressed(self): + self.duplicate_requested.emit() + + def _on_expected_selection_change(self, event): + if event["workfile_name_selected"]: + return + + workfile_name = event["workfile_name"] + if ( + workfile_name is not None + and workfile_name != self._get_selected_info()["filename"] + ): + index = self._model.get_index_by_filename(workfile_name) + if index.isValid(): + proxy_index = self._proxy_model.mapFromSource(index) + self._view.setCurrentIndex(proxy_index) + + self._controller.expected_workfile_selected( + event["folder_id"], event["task_name"], workfile_name + ) diff --git a/openpype/tools/ayon_workfiles/widgets/folders_widget.py b/openpype/tools/ayon_workfiles/widgets/folders_widget.py new file mode 100644 index 0000000000..b35845f4b6 --- /dev/null +++ b/openpype/tools/ayon_workfiles/widgets/folders_widget.py @@ -0,0 +1,324 @@ +import uuid +import collections + +import qtawesome +from qtpy import QtWidgets, QtGui, QtCore + +from openpype.tools.utils import ( + RecursiveSortFilterProxyModel, + DeselectableTreeView, +) + +from .constants import ITEM_ID_ROLE, ITEM_NAME_ROLE + +SENDER_NAME = "qt_folders_model" + + +class FoldersRefreshThread(QtCore.QThread): + """Thread for refreshing folders. + + Call controller to get folders and emit signal when finished. + + Args: + controller (AbstractWorkfilesFrontend): The control object. + """ + + refresh_finished = QtCore.Signal(str) + + def __init__(self, controller): + super(FoldersRefreshThread, self).__init__() + self._id = uuid.uuid4().hex + self._controller = controller + self._result = None + + @property + def id(self): + """Thread id. + + Returns: + str: Unique id of the thread. + """ + + return self._id + + def run(self): + self._result = self._controller.get_folder_items(SENDER_NAME) + self.refresh_finished.emit(self.id) + + def get_result(self): + return self._result + + +class FoldersModel(QtGui.QStandardItemModel): + """Folders model which cares about refresh of folders. + + Args: + controller (AbstractWorkfilesFrontend): The control object. + """ + + refreshed = QtCore.Signal() + + def __init__(self, controller): + super(FoldersModel, self).__init__() + + self._controller = controller + self._items_by_id = {} + self._parent_id_by_id = {} + + self._refresh_threads = {} + self._current_refresh_thread = None + + self._has_content = False + self._is_refreshing = False + + @property + def is_refreshing(self): + """Model is refreshing. + + Returns: + bool: True if model is refreshing. + """ + return self._is_refreshing + + @property + def has_content(self): + """Has at least one folder. + + Returns: + bool: True if model has at least one folder. + """ + + return self._has_content + + def clear(self): + self._items_by_id = {} + self._parent_id_by_id = {} + self._has_content = False + super(FoldersModel, self).clear() + + def get_index_by_id(self, item_id): + """Get index by folder id. + + Returns: + QtCore.QModelIndex: Index of the folder. Can be invalid if folder + is not available. + """ + item = self._items_by_id.get(item_id) + if item is None: + return QtCore.QModelIndex() + return self.indexFromItem(item) + + def refresh(self): + """Refresh folders items. + + Refresh start thread because it can cause that controller can + start query from database if folders are not cached. + """ + + self._is_refreshing = True + + thread = FoldersRefreshThread(self._controller) + self._current_refresh_thread = thread.id + self._refresh_threads[thread.id] = thread + thread.refresh_finished.connect(self._on_refresh_thread) + thread.start() + + def _on_refresh_thread(self, thread_id): + """Callback when refresh thread is finished. + + Technically can be running multiple refresh threads at the same time, + to avoid using values from wrong thread, we check if thread id is + current refresh thread id. + + Folders are stored by id. + + Args: + thread_id (str): Thread id. + """ + + thread = self._refresh_threads.pop(thread_id) + if thread_id != self._current_refresh_thread: + return + + folder_items_by_id = thread.get_result() + if not folder_items_by_id: + if folder_items_by_id is not None: + self.clear() + self._is_refreshing = False + return + + self._has_content = True + + folder_ids = set(folder_items_by_id) + ids_to_remove = set(self._items_by_id) - folder_ids + + folder_items_by_parent = collections.defaultdict(list) + for folder_item in folder_items_by_id.values(): + folder_items_by_parent[folder_item.parent_id].append(folder_item) + + hierarchy_queue = collections.deque() + hierarchy_queue.append(None) + + while hierarchy_queue: + parent_id = hierarchy_queue.popleft() + folder_items = folder_items_by_parent[parent_id] + if parent_id is None: + parent_item = self.invisibleRootItem() + else: + parent_item = self._items_by_id[parent_id] + + new_items = [] + for folder_item in folder_items: + item_id = folder_item.entity_id + item = self._items_by_id.get(item_id) + if item is None: + is_new = True + item = QtGui.QStandardItem() + item.setEditable(False) + else: + is_new = self._parent_id_by_id[item_id] != parent_id + + icon = qtawesome.icon( + folder_item.icon_name, + color=folder_item.icon_color, + ) + item.setData(item_id, ITEM_ID_ROLE) + item.setData(folder_item.name, ITEM_NAME_ROLE) + item.setData(folder_item.label, QtCore.Qt.DisplayRole) + item.setData(icon, QtCore.Qt.DecorationRole) + if is_new: + new_items.append(item) + self._items_by_id[item_id] = item + self._parent_id_by_id[item_id] = parent_id + + hierarchy_queue.append(item_id) + + if new_items: + parent_item.appendRows(new_items) + + for item_id in ids_to_remove: + item = self._items_by_id[item_id] + parent_id = self._parent_id_by_id[item_id] + if parent_id is None: + parent_item = self.invisibleRootItem() + else: + parent_item = self._items_by_id[parent_id] + parent_item.takeChild(item.row()) + + for item_id in ids_to_remove: + self._items_by_id.pop(item_id) + self._parent_id_by_id.pop(item_id) + + self._is_refreshing = False + self.refreshed.emit() + + +class FoldersWidget(QtWidgets.QWidget): + """Folders widget. + + Widget that handles folders view, model and selection. + + Args: + controller (AbstractWorkfilesFrontend): The control object. + parent (QtWidgets.QWidget): The parent widget. + """ + + def __init__(self, controller, parent): + super(FoldersWidget, self).__init__(parent) + + folders_view = DeselectableTreeView(self) + folders_view.setHeaderHidden(True) + + folders_model = FoldersModel(controller) + folders_proxy_model = RecursiveSortFilterProxyModel() + folders_proxy_model.setSourceModel(folders_model) + + folders_view.setModel(folders_proxy_model) + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(folders_view, 1) + + controller.register_event_callback( + "folders.refresh.finished", + self._on_folders_refresh_finished + ) + controller.register_event_callback( + "controller.refresh.finished", + self._on_controller_refresh + ) + controller.register_event_callback( + "expected_selection_changed", + self._on_expected_selection_change + ) + + selection_model = folders_view.selectionModel() + selection_model.selectionChanged.connect(self._on_selection_change) + + folders_model.refreshed.connect(self._on_model_refresh) + + self._controller = controller + self._folders_view = folders_view + self._folders_model = folders_model + self._folders_proxy_model = folders_proxy_model + + self._expected_selection = None + + def set_name_filer(self, name): + self._folders_proxy_model.setFilterFixedString(name) + + def _clear(self): + self._folders_model.clear() + + def _on_folders_refresh_finished(self, event): + if event["sender"] != SENDER_NAME: + self._folders_model.refresh() + + def _on_controller_refresh(self): + self._update_expected_selection() + + def _update_expected_selection(self, expected_data=None): + if expected_data is None: + expected_data = self._controller.get_expected_selection_data() + + # We're done + if expected_data["folder_selected"]: + return + + folder_id = expected_data["folder_id"] + self._expected_selection = folder_id + if not self._folders_model.is_refreshing: + self._set_expected_selection() + + def _set_expected_selection(self): + folder_id = self._expected_selection + self._expected_selection = None + if ( + folder_id is not None + and folder_id != self._get_selected_item_id() + ): + index = self._folders_model.get_index_by_id(folder_id) + if index.isValid(): + proxy_index = self._folders_proxy_model.mapFromSource(index) + self._folders_view.setCurrentIndex(proxy_index) + self._controller.expected_folder_selected(folder_id) + + def _on_model_refresh(self): + if self._expected_selection: + self._set_expected_selection() + self._folders_proxy_model.sort(0) + + def _on_expected_selection_change(self, event): + self._update_expected_selection(event.data) + + def _get_selected_item_id(self): + selection_model = self._folders_view.selectionModel() + for index in selection_model.selectedIndexes(): + item_id = index.data(ITEM_ID_ROLE) + if item_id is not None: + return item_id + return None + + def _on_selection_change(self): + item_id = self._get_selected_item_id() + self._controller.set_selected_folder(item_id) diff --git a/openpype/tools/ayon_workfiles/widgets/save_as_dialog.py b/openpype/tools/ayon_workfiles/widgets/save_as_dialog.py new file mode 100644 index 0000000000..cdce73f030 --- /dev/null +++ b/openpype/tools/ayon_workfiles/widgets/save_as_dialog.py @@ -0,0 +1,351 @@ +from qtpy import QtWidgets, QtCore + +from openpype.tools.utils import PlaceholderLineEdit + + +class SubversionLineEdit(QtWidgets.QWidget): + """QLineEdit with QPushButton for drop down selection of list of strings""" + + text_changed = QtCore.Signal(str) + + def __init__(self, *args, **kwargs): + super(SubversionLineEdit, self).__init__(*args, **kwargs) + + input_field = PlaceholderLineEdit(self) + menu_btn = QtWidgets.QPushButton(self) + menu_btn.setFixedWidth(18) + + menu = QtWidgets.QMenu(self) + menu_btn.setMenu(menu) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(3) + + layout.addWidget(input_field, 1) + layout.addWidget(menu_btn, 0) + + input_field.textChanged.connect(self.text_changed) + + self.setFocusProxy(input_field) + + self._input_field = input_field + self._menu_btn = menu_btn + self._menu = menu + + def set_placeholder(self, placeholder): + self._input_field.setPlaceholderText(placeholder) + + def set_text(self, text): + self._input_field.setText(text) + + def set_values(self, values): + self._update(values) + + def _on_button_clicked(self): + self._menu.exec_() + + def _on_action_clicked(self, action): + self._input_field.setText(action.text()) + + def _update(self, values): + """Create optional predefined subset names + + Args: + default_names(list): all predefined names + + Returns: + None + """ + + menu = self._menu + button = self._menu_btn + + state = any(values) + button.setEnabled(state) + if state is False: + return + + # Include an empty string + values = [""] + sorted(values) + + # Get and destroy the action group + group = button.findChild(QtWidgets.QActionGroup) + if group: + group.deleteLater() + + # Build new action group + group = QtWidgets.QActionGroup(button) + for name in values: + action = group.addAction(name) + menu.addAction(action) + + group.triggered.connect(self._on_action_clicked) + + +class SaveAsDialog(QtWidgets.QDialog): + """Save as dialog to define a unique filename inside workdir. + + The filename is calculated in controller where UI sends values from + dialog inputs. + + Args: + controller (AbstractWorkfilesFrontend): The control object. + """ + + def __init__(self, controller, parent): + super(SaveAsDialog, self).__init__(parent=parent) + self.setWindowFlags(self.windowFlags() | QtCore.Qt.FramelessWindowHint) + + self._controller = controller + + self._folder_id = None + self._task_id = None + self._last_version = None + self._template_key = None + self._comment_value = None + self._version_value = None + self._ext_value = None + self._filename = None + self._workdir = None + + self._result = None + + # Btns widget + btns_widget = QtWidgets.QWidget(self) + + btn_ok = QtWidgets.QPushButton("Ok", btns_widget) + btn_cancel = QtWidgets.QPushButton("Cancel", btns_widget) + + btns_layout = QtWidgets.QHBoxLayout(btns_widget) + btns_layout.addWidget(btn_ok) + btns_layout.addWidget(btn_cancel) + + # Inputs widget + inputs_widget = QtWidgets.QWidget(self) + + # Version widget + version_widget = QtWidgets.QWidget(inputs_widget) + + # Version number input + version_input = QtWidgets.QSpinBox(version_widget) + version_input.setMinimum(1) + version_input.setMaximum(9999) + + # Last version checkbox + last_version_check = QtWidgets.QCheckBox( + "Next Available Version", version_widget + ) + last_version_check.setChecked(True) + + version_layout = QtWidgets.QHBoxLayout(version_widget) + version_layout.setContentsMargins(0, 0, 0, 0) + version_layout.addWidget(version_input) + version_layout.addWidget(last_version_check) + + # Preview widget + preview_widget = QtWidgets.QLabel("Preview filename", inputs_widget) + preview_widget.setWordWrap(True) + + # Subversion input + subversion_input = SubversionLineEdit(inputs_widget) + subversion_input.set_placeholder("Will be part of filename.") + + # Extensions combobox + extension_combobox = QtWidgets.QComboBox(inputs_widget) + # Add styled delegate to use stylesheets + extension_delegate = QtWidgets.QStyledItemDelegate() + extension_combobox.setItemDelegate(extension_delegate) + + version_label = QtWidgets.QLabel("Version:", inputs_widget) + subversion_label = QtWidgets.QLabel("Subversion:", inputs_widget) + extension_label = QtWidgets.QLabel("Extension:", inputs_widget) + preview_label = QtWidgets.QLabel("Preview:", inputs_widget) + + # Build inputs + inputs_layout = QtWidgets.QGridLayout(inputs_widget) + inputs_layout.addWidget(version_label, 0, 0) + inputs_layout.addWidget(version_widget, 0, 1) + inputs_layout.addWidget(subversion_label, 1, 0) + inputs_layout.addWidget(subversion_input, 1, 1) + inputs_layout.addWidget(extension_label, 2, 0) + inputs_layout.addWidget(extension_combobox, 2, 1) + inputs_layout.addWidget(preview_label, 3, 0) + inputs_layout.addWidget(preview_widget, 3, 1) + + # Build layout + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.addWidget(inputs_widget) + main_layout.addWidget(btns_widget) + + # Signal callback registration + version_input.valueChanged.connect(self._on_version_spinbox_change) + last_version_check.stateChanged.connect( + self._on_version_checkbox_change + ) + + subversion_input.text_changed.connect(self._on_comment_change) + extension_combobox.currentIndexChanged.connect( + self._on_extension_change) + + btn_ok.pressed.connect(self._on_ok_pressed) + btn_cancel.pressed.connect(self._on_cancel_pressed) + + # Store objects + self._inputs_layout = inputs_layout + + self._btn_ok = btn_ok + self._btn_cancel = btn_cancel + + self._version_widget = version_widget + + self._version_input = version_input + self._last_version_check = last_version_check + + self._extension_delegate = extension_delegate + self._extension_combobox = extension_combobox + self._subversion_input = subversion_input + self._preview_widget = preview_widget + + self._version_label = version_label + self._subversion_label = subversion_label + self._extension_label = extension_label + self._preview_label = preview_label + + # Post init setup + + # Allow "Enter" key to accept the save. + btn_ok.setDefault(True) + + # Disable version input if last version is checked + version_input.setEnabled(not last_version_check.isChecked()) + + # Force default focus to comment, some hosts didn't automatically + # apply focus to this line edit (e.g. Houdini) + subversion_input.setFocus() + + def get_result(self): + return self._result + + def update_context(self): + # Add version only if template contains version key + # - since the version can be padded with "{version:0>4}" we only search + # for "{version". + selected_context = self._controller.get_selected_context() + folder_id = selected_context["folder_id"] + task_id = selected_context["task_id"] + data = self._controller.get_workarea_save_as_data(folder_id, task_id) + last_version = data["last_version"] + comment = data["comment"] + comment_hints = data["comment_hints"] + + template_has_version = data["template_has_version"] + template_has_comment = data["template_has_comment"] + + self._folder_id = folder_id + self._task_id = task_id + self._workdir = data["workdir"] + self._comment_value = data["comment"] + self._ext_value = data["ext"] + self._template_key = data["template_key"] + self._last_version = data["last_version"] + + self._extension_combobox.clear() + self._extension_combobox.addItems(data["extensions"]) + + self._version_input.setValue(last_version) + + vw_idx = self._inputs_layout.indexOf(self._version_widget) + self._version_label.setVisible(template_has_version) + self._version_widget.setVisible(template_has_version) + if template_has_version: + if vw_idx == -1: + self._inputs_layout.addWidget(self._version_label, 0, 0) + self._inputs_layout.addWidget(self._version_widget, 0, 1) + elif vw_idx != -1: + self._inputs_layout.takeAt(vw_idx) + self._inputs_layout.takeAt( + self._inputs_layout.indexOf(self._version_label) + ) + + cw_idx = self._inputs_layout.indexOf(self._subversion_input) + self._subversion_label.setVisible(template_has_comment) + self._subversion_input.setVisible(template_has_comment) + if template_has_comment: + if cw_idx == -1: + self._inputs_layout.addWidget(self._subversion_label, 1, 0) + self._inputs_layout.addWidget(self._subversion_input, 1, 1) + elif cw_idx != -1: + self._inputs_layout.takeAt(cw_idx) + self._inputs_layout.takeAt( + self._inputs_layout.indexOf(self._subversion_label) + ) + + if template_has_comment: + self._subversion_input.set_text(comment or "") + self._subversion_input.set_values(comment_hints) + self._update_filename() + + def _on_version_spinbox_change(self, value): + if value == self._version_value: + return + self._version_value = value + if not self._last_version_check.isChecked(): + self._update_filename() + + def _on_version_checkbox_change(self): + use_last_version = self._last_version_check.isChecked() + self._version_input.setEnabled(not use_last_version) + if use_last_version: + self._version_input.blockSignals(True) + self._version_input.setValue(self._last_version) + self._version_input.blockSignals(False) + self._update_filename() + + def _on_comment_change(self, text): + if self._comment_value == text: + return + self._comment_value = text + self._update_filename() + + def _on_extension_change(self): + ext = self._extension_combobox.currentText() + if ext == self._ext_value: + return + self._ext_value = ext + self._update_filename() + + def _on_ok_pressed(self): + self._result = { + "filename": self._filename, + "workdir": self._workdir, + "folder_id": self._folder_id, + "task_id": self._task_id, + "template_key": self._template_key, + } + self.close() + + def _on_cancel_pressed(self): + self.close() + + def _update_filename(self): + result = self._controller.fill_workarea_filepath( + self._folder_id, + self._task_id, + self._ext_value, + self._last_version_check.isChecked(), + self._version_value, + self._comment_value, + ) + self._filename = result.filename + self._btn_ok.setEnabled(not result.exists) + + if result.exists: + self._preview_widget.setText(( + "Cannot create \"{}\" because file exists!" + "" + ).format(result.filename)) + else: + self._preview_widget.setText( + "{}".format(result.filename) + ) diff --git a/openpype/tools/ayon_workfiles/widgets/side_panel.py b/openpype/tools/ayon_workfiles/widgets/side_panel.py new file mode 100644 index 0000000000..7f06576a00 --- /dev/null +++ b/openpype/tools/ayon_workfiles/widgets/side_panel.py @@ -0,0 +1,163 @@ +import datetime + +from qtpy import QtWidgets, QtCore + + +def file_size_to_string(file_size): + size = 0 + size_ending_mapping = { + "KB": 1024 ** 1, + "MB": 1024 ** 2, + "GB": 1024 ** 3 + } + ending = "B" + for _ending, _size in size_ending_mapping.items(): + if file_size < _size: + break + size = file_size / _size + ending = _ending + return "{:.2f} {}".format(size, ending) + + +class SidePanelWidget(QtWidgets.QWidget): + """Details about selected workfile. + + Todos: + At this moment only shows created and modified date of file + or its size. + + Args: + controller (AbstractWorkfilesFrontend): The control object. + parent (QtWidgets.QWidget): The parent widget. + """ + + published_workfile_message = ( + "INFO: Opened published workfiles will be stored in" + " temp directory on your machine. Current temp size: {}." + ) + + def __init__(self, controller, parent): + super(SidePanelWidget, self).__init__(parent) + + details_label = QtWidgets.QLabel("Details", self) + details_input = QtWidgets.QPlainTextEdit(self) + details_input.setReadOnly(True) + + artist_note_widget = QtWidgets.QWidget(self) + note_label = QtWidgets.QLabel("Artist note", artist_note_widget) + note_input = QtWidgets.QPlainTextEdit(artist_note_widget) + btn_note_save = QtWidgets.QPushButton("Save note", artist_note_widget) + + artist_note_layout = QtWidgets.QVBoxLayout(artist_note_widget) + artist_note_layout.setContentsMargins(0, 0, 0, 0) + artist_note_layout.addWidget(note_label, 0) + artist_note_layout.addWidget(note_input, 1) + artist_note_layout.addWidget( + btn_note_save, 0, alignment=QtCore.Qt.AlignRight + ) + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(details_label, 0) + main_layout.addWidget(details_input, 1) + main_layout.addWidget(artist_note_widget, 1) + + note_input.textChanged.connect(self._on_note_change) + btn_note_save.clicked.connect(self._on_save_click) + + controller.register_event_callback( + "workarea.selection.changed", self._on_selection_change + ) + + self._details_input = details_input + self._artist_note_widget = artist_note_widget + self._note_input = note_input + self._btn_note_save = btn_note_save + + self._folder_id = None + self._task_id = None + self._filepath = None + self._orig_note = "" + self._controller = controller + + self._set_context(None, None, None) + + def set_published_mode(self, published_mode): + """Change published mode. + + Args: + published_mode (bool): Published mode enabled. + """ + + self._artist_note_widget.setVisible(not published_mode) + + def _on_selection_change(self, event): + folder_id = event["folder_id"] + task_id = event["task_id"] + filepath = event["path"] + + self._set_context(folder_id, task_id, filepath) + + def _on_note_change(self): + text = self._note_input.toPlainText() + self._btn_note_save.setEnabled(self._orig_note != text) + + def _on_save_click(self): + note = self._note_input.toPlainText() + self._controller.save_workfile_info( + self._folder_id, + self._task_id, + self._filepath, + note + ) + self._orig_note = note + self._btn_note_save.setEnabled(False) + + def _set_context(self, folder_id, task_id, filepath): + workfile_info = None + # Check if folder, task and file are selected + if bool(folder_id) and bool(task_id) and bool(filepath): + workfile_info = self._controller.get_workfile_info( + folder_id, task_id, filepath + ) + enabled = workfile_info is not None + + self._details_input.setEnabled(enabled) + self._note_input.setEnabled(enabled) + self._btn_note_save.setEnabled(enabled) + + self._folder_id = folder_id + self._task_id = task_id + self._filepath = filepath + + # Disable inputs and remove texts if any required arguments are + # missing + if not enabled: + self._orig_note = "" + self._details_input.setPlainText("") + self._note_input.setPlainText("") + return + + note = workfile_info.note + size_value = file_size_to_string(workfile_info.filesize) + + # Append html string + datetime_format = "%b %d %Y %H:%M:%S" + creation_time = datetime.datetime.fromtimestamp( + workfile_info.creation_time) + modification_time = datetime.datetime.fromtimestamp( + workfile_info.modification_time) + lines = ( + "Size:", + size_value, + "Created:", + creation_time.strftime(datetime_format), + "Modified:", + modification_time.strftime(datetime_format) + ) + self._orig_note = note + self._note_input.setPlainText(note) + + # Set as empty string + self._details_input.setPlainText("") + self._details_input.appendHtml("
".join(lines)) diff --git a/openpype/tools/ayon_workfiles/widgets/tasks_widget.py b/openpype/tools/ayon_workfiles/widgets/tasks_widget.py new file mode 100644 index 0000000000..04f5b286b1 --- /dev/null +++ b/openpype/tools/ayon_workfiles/widgets/tasks_widget.py @@ -0,0 +1,420 @@ +import uuid +import qtawesome +from qtpy import QtWidgets, QtGui, QtCore + +from openpype.style import get_disabled_entity_icon_color +from openpype.tools.utils import DeselectableTreeView + +from .constants import ( + ITEM_NAME_ROLE, + ITEM_ID_ROLE, + PARENT_ID_ROLE, +) + +SENDER_NAME = "qt_tasks_model" + + +class RefreshThread(QtCore.QThread): + """Thread for refreshing tasks. + + Call controller to get tasks and emit signal when finished. + + Args: + controller (AbstractWorkfilesFrontend): The control object. + folder_id (str): Folder id. + """ + + refresh_finished = QtCore.Signal(str) + + def __init__(self, controller, folder_id): + super(RefreshThread, self).__init__() + self._id = uuid.uuid4().hex + self._controller = controller + self._folder_id = folder_id + self._result = None + + @property + def id(self): + return self._id + + def run(self): + self._result = self._controller.get_task_items( + self._folder_id, SENDER_NAME) + self.refresh_finished.emit(self.id) + + def get_result(self): + return self._result + + +class TasksModel(QtGui.QStandardItemModel): + """Tasks model which cares about refresh of tasks by folder id. + + Args: + controller (AbstractWorkfilesFrontend): The control object. + """ + + refreshed = QtCore.Signal() + + def __init__(self, controller): + super(TasksModel, self).__init__() + + self._controller = controller + + self._items_by_name = {} + self._has_content = False + self._is_refreshing = False + + self._invalid_selection_item_used = False + self._invalid_selection_item = None + self._empty_tasks_item_used = False + self._empty_tasks_item = None + + self._last_folder_id = None + + self._refresh_threads = {} + self._current_refresh_thread = None + + # Initial state + self._add_invalid_selection_item() + + def clear(self): + self._items_by_name = {} + self._has_content = False + self._remove_invalid_items() + super(TasksModel, self).clear() + + def refresh(self, folder_id): + """Refresh tasks for folder. + + Args: + folder_id (Union[str, None]): Folder id. + """ + + self._refresh(folder_id) + + def get_index_by_name(self, task_name): + """Find item by name and return its index. + + Returns: + QtCore.QModelIndex: Index of item. Is invalid if task is not + found by name. + """ + + item = self._items_by_name.get(task_name) + if item is None: + return QtCore.QModelIndex() + return self.indexFromItem(item) + + def get_last_folder_id(self): + """Get last refreshed folder id. + + Returns: + Union[str, None]: Folder id. + """ + + return self._last_folder_id + + def _get_invalid_selection_item(self): + if self._invalid_selection_item is None: + item = QtGui.QStandardItem("Select a folder") + item.setFlags(QtCore.Qt.NoItemFlags) + icon = qtawesome.icon( + "fa.times", + color=get_disabled_entity_icon_color() + ) + item.setData(icon, QtCore.Qt.DecorationRole) + self._invalid_selection_item = item + return self._invalid_selection_item + + def _get_empty_task_item(self): + if self._empty_tasks_item is None: + item = QtGui.QStandardItem("No task") + icon = qtawesome.icon( + "fa.exclamation-circle", + color=get_disabled_entity_icon_color() + ) + item.setData(icon, QtCore.Qt.DecorationRole) + item.setFlags(QtCore.Qt.NoItemFlags) + self._empty_tasks_item = item + return self._empty_tasks_item + + def _add_invalid_item(self, item): + self.clear() + root_item = self.invisibleRootItem() + root_item.appendRow(item) + + def _remove_invalid_item(self, item): + root_item = self.invisibleRootItem() + root_item.takeRow(item.row()) + + def _remove_invalid_items(self): + self._remove_invalid_selection_item() + self._remove_empty_task_item() + + def _add_invalid_selection_item(self): + if not self._invalid_selection_item_used: + self._add_invalid_item(self._get_invalid_selection_item()) + self._invalid_selection_item_used = True + + def _remove_invalid_selection_item(self): + if self._invalid_selection_item: + self._remove_invalid_item(self._get_invalid_selection_item()) + self._invalid_selection_item_used = False + + def _add_empty_task_item(self): + if not self._empty_tasks_item_used: + self._add_invalid_item(self._get_empty_task_item()) + self._empty_tasks_item_used = True + + def _remove_empty_task_item(self): + if self._empty_tasks_item_used: + self._remove_invalid_item(self._get_empty_task_item()) + self._empty_tasks_item_used = False + + def _refresh(self, folder_id): + self._is_refreshing = True + self._last_folder_id = folder_id + if not folder_id: + self._add_invalid_selection_item() + self._current_refresh_thread = None + self._is_refreshing = False + self.refreshed.emit() + return + + thread = RefreshThread(self._controller, folder_id) + self._current_refresh_thread = thread.id + self._refresh_threads[thread.id] = thread + thread.refresh_finished.connect(self._on_refresh_thread) + thread.start() + + def _on_refresh_thread(self, thread_id): + """Callback when refresh thread is finished. + + Technically can be running multiple refresh threads at the same time, + to avoid using values from wrong thread, we check if thread id is + current refresh thread id. + + Tasks are stored by name, so if a folder has same task name as + previously selected folder it keeps the selection. + + Args: + thread_id (str): Thread id. + """ + + thread = self._refresh_threads.pop(thread_id) + if thread_id != self._current_refresh_thread: + return + + task_items = thread.get_result() + # Task items are refreshed + if task_items is None: + return + + # No tasks are available on folder + if not task_items: + self._add_empty_task_item() + return + self._remove_invalid_items() + + new_items = [] + new_names = set() + for task_item in task_items: + name = task_item.name + new_names.add(name) + item = self._items_by_name.get(name) + if item is None: + item = QtGui.QStandardItem() + item.setEditable(False) + new_items.append(item) + self._items_by_name[name] = item + + # TODO cache locally + icon = qtawesome.icon( + task_item.icon_name, + color=task_item.icon_color, + ) + item.setData(task_item.label, QtCore.Qt.DisplayRole) + item.setData(name, ITEM_NAME_ROLE) + item.setData(task_item.id, ITEM_ID_ROLE) + item.setData(task_item.parent_id, PARENT_ID_ROLE) + item.setData(icon, QtCore.Qt.DecorationRole) + + root_item = self.invisibleRootItem() + + for name in set(self._items_by_name) - new_names: + item = self._items_by_name.pop(name) + root_item.removeRow(item.row()) + + if new_items: + root_item.appendRows(new_items) + + self._has_content = root_item.rowCount() > 0 + self._is_refreshing = False + self.refreshed.emit() + + @property + def is_refreshing(self): + """Model is refreshing. + + Returns: + bool: Model is refreshing + """ + + return self._is_refreshing + + @property + def has_content(self): + """Model has content. + + Returns: + bools: Have at least one task. + """ + + return self._has_content + + def headerData(self, section, orientation, role): + # Show nice labels in the header + if ( + role == QtCore.Qt.DisplayRole + and orientation == QtCore.Qt.Horizontal + ): + if section == 0: + return "Tasks" + + return super(TasksModel, self).headerData( + section, orientation, role + ) + + +class TasksWidget(QtWidgets.QWidget): + """Tasks widget. + + Widget that handles tasks view, model and selection. + + Args: + controller (AbstractWorkfilesFrontend): Workfiles controller. + """ + + def __init__(self, controller, parent): + super(TasksWidget, self).__init__(parent) + + tasks_view = DeselectableTreeView(self) + tasks_view.setIndentation(0) + + tasks_model = TasksModel(controller) + tasks_proxy_model = QtCore.QSortFilterProxyModel() + tasks_proxy_model.setSourceModel(tasks_model) + + tasks_view.setModel(tasks_proxy_model) + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(tasks_view, 1) + + controller.register_event_callback( + "tasks.refresh.finished", + self._on_tasks_refresh_finished + ) + controller.register_event_callback( + "selection.folder.changed", + self._folder_selection_changed + ) + controller.register_event_callback( + "expected_selection_changed", + self._on_expected_selection_change + ) + + selection_model = tasks_view.selectionModel() + selection_model.selectionChanged.connect(self._on_selection_change) + + tasks_model.refreshed.connect(self._on_tasks_model_refresh) + + self._controller = controller + self._tasks_view = tasks_view + self._tasks_model = tasks_model + self._tasks_proxy_model = tasks_proxy_model + + self._selected_folder_id = None + + self._expected_selection_data = None + + def _clear(self): + self._tasks_model.clear() + + def _on_tasks_refresh_finished(self, event): + """Tasks were refreshed in controller. + + Ignore if refresh was triggered by tasks model, or refreshed folder is + not the same as currently selected folder. + + Args: + event (Event): Event object. + """ + + # Refresh only if current folder id is the same + if ( + event["sender"] == SENDER_NAME + or event["folder_id"] != self._selected_folder_id + ): + return + self._tasks_model.refresh(self._selected_folder_id) + + def _folder_selection_changed(self, event): + self._selected_folder_id = event["folder_id"] + self._tasks_model.refresh(self._selected_folder_id) + + def _on_tasks_model_refresh(self): + if not self._set_expected_selection(): + self._on_selection_change() + self._tasks_proxy_model.sort(0) + + def _set_expected_selection(self): + if self._expected_selection_data is None: + return False + folder_id = self._expected_selection_data["folder_id"] + task_name = self._expected_selection_data["task_name"] + self._expected_selection_data = None + model_folder_id = self._tasks_model.get_last_folder_id() + if folder_id != model_folder_id: + return False + if task_name is not None: + index = self._tasks_model.get_index_by_name(task_name) + if index.isValid(): + proxy_index = self._tasks_proxy_model.mapFromSource(index) + self._tasks_view.setCurrentIndex(proxy_index) + self._controller.expected_task_selected(folder_id, task_name) + return True + + def _on_expected_selection_change(self, event): + if event["task_selected"] or not event["folder_selected"]: + return + + model_folder_id = self._tasks_model.get_last_folder_id() + folder_id = event["folder_id"] + self._expected_selection_data = { + "task_name": event["task_name"], + "folder_id": folder_id, + } + + if folder_id != model_folder_id or self._tasks_model.is_refreshing: + return + self._set_expected_selection() + + def _get_selected_item_ids(self): + selection_model = self._tasks_view.selectionModel() + for index in selection_model.selectedIndexes(): + task_id = index.data(ITEM_ID_ROLE) + task_name = index.data(ITEM_NAME_ROLE) + parent_id = index.data(PARENT_ID_ROLE) + if task_name is not None: + return parent_id, task_id, task_name + return self._selected_folder_id, None, None + + def _on_selection_change(self): + # Don't trigger task change during refresh + # - a task was deselected if that happens + # - can cause crash triggered during tasks refreshing + if self._tasks_model.is_refreshing: + return + parent_id, task_id, task_name = self._get_selected_item_ids() + self._controller.set_selected_task(parent_id, task_id, task_name) diff --git a/openpype/tools/ayon_workfiles/widgets/utils.py b/openpype/tools/ayon_workfiles/widgets/utils.py new file mode 100644 index 0000000000..6a61239f8d --- /dev/null +++ b/openpype/tools/ayon_workfiles/widgets/utils.py @@ -0,0 +1,94 @@ +from qtpy import QtWidgets, QtCore +from openpype.tools.flickcharm import FlickCharm + + +class TreeView(QtWidgets.QTreeView): + """Ultimate TreeView with flick charm and double click signals. + + Tree view have deselectable mode, which allows to deselect items by + clicking on item area without any items. + + Todos: + Add to tools utils. + """ + + double_clicked_left = QtCore.Signal() + double_clicked_right = QtCore.Signal() + + def __init__(self, *args, **kwargs): + super(TreeView, self).__init__(*args, **kwargs) + self._deselectable = False + + self._flick_charm_activated = False + self._flick_charm = FlickCharm(parent=self) + self._before_flick_scroll_mode = None + + def is_deselectable(self): + return self._deselectable + + def set_deselectable(self, deselectable): + self._deselectable = deselectable + + deselectable = property(is_deselectable, set_deselectable) + + def mousePressEvent(self, event): + if self._deselectable: + index = self.indexAt(event.pos()) + if not index.isValid(): + # clear the selection + self.clearSelection() + # clear the current index + self.setCurrentIndex(QtCore.QModelIndex()) + super(TreeView, self).mousePressEvent(event) + + def mouseDoubleClickEvent(self, event): + if event.button() == QtCore.Qt.LeftButton: + self.double_clicked_left.emit() + + elif event.button() == QtCore.Qt.RightButton: + self.double_clicked_right.emit() + + return super(TreeView, self).mouseDoubleClickEvent(event) + + def activate_flick_charm(self): + if self._flick_charm_activated: + return + self._flick_charm_activated = True + self._before_flick_scroll_mode = self.verticalScrollMode() + self._flick_charm.activateOn(self) + self.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) + + def deactivate_flick_charm(self): + if not self._flick_charm_activated: + return + self._flick_charm_activated = False + self._flick_charm.deactivateFrom(self) + if self._before_flick_scroll_mode is not None: + self.setVerticalScrollMode(self._before_flick_scroll_mode) + + +class BaseOverlayFrame(QtWidgets.QFrame): + """Base frame for overlay widgets. + + Has implemented automated resize and event filtering. + """ + + def __init__(self, parent): + super(BaseOverlayFrame, self).__init__(parent) + self.setObjectName("OverlayFrame") + + self._parent = parent + + def setVisible(self, visible): + super(BaseOverlayFrame, self).setVisible(visible) + if visible: + self._parent.installEventFilter(self) + self.resize(self._parent.size()) + else: + self._parent.removeEventFilter(self) + + def eventFilter(self, obj, event): + if event.type() == QtCore.QEvent.Resize: + self.resize(obj.size()) + + return super(BaseOverlayFrame, self).eventFilter(obj, event) diff --git a/openpype/tools/ayon_workfiles/widgets/window.py b/openpype/tools/ayon_workfiles/widgets/window.py new file mode 100644 index 0000000000..ef352c8b18 --- /dev/null +++ b/openpype/tools/ayon_workfiles/widgets/window.py @@ -0,0 +1,400 @@ +from qtpy import QtCore, QtWidgets, QtGui + +from openpype import style, resources +from openpype.tools.utils import ( + PlaceholderLineEdit, + MessageOverlayObject, +) +from openpype.tools.utils.lib import get_qta_icon_by_name_and_color + +from openpype.tools.ayon_workfiles.control import BaseWorkfileController + +from .side_panel import SidePanelWidget +from .folders_widget import FoldersWidget +from .tasks_widget import TasksWidget +from .files_widget import FilesWidget +from .utils import BaseOverlayFrame + + +# TODO move to utils +# from openpype.tools.utils.lib import ( +# get_refresh_icon, get_go_to_current_icon) +def get_refresh_icon(): + return get_qta_icon_by_name_and_color( + "fa.refresh", style.get_default_tools_icon_color() + ) + + +def get_go_to_current_icon(): + return get_qta_icon_by_name_and_color( + "fa.arrow-down", style.get_default_tools_icon_color() + ) + + +class InvalidHostOverlay(BaseOverlayFrame): + def __init__(self, parent): + super(InvalidHostOverlay, self).__init__(parent) + + label_widget = QtWidgets.QLabel( + ( + "Workfiles tool is not supported in this host/DCCs." + "

This may be caused by a bug." + " Please contact your TD for more information." + ), + self + ) + label_widget.setAlignment(QtCore.Qt.AlignCenter) + label_widget.setObjectName("OverlayFrameLabel") + + layout = QtWidgets.QVBoxLayout(self) + layout.addStretch(2) + layout.addWidget(label_widget, 0, QtCore.Qt.AlignCenter) + layout.addStretch(3) + + label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + +class WorkfilesToolWindow(QtWidgets.QWidget): + """WorkFiles Window. + + Main windows of workfiles tool. + + Args: + controller (AbstractWorkfilesFrontend): Frontend controller. + parent (Optional[QtWidgets.QWidget]): Parent widget. + """ + + title = "Work Files" + + def __init__(self, controller=None, parent=None): + super(WorkfilesToolWindow, self).__init__(parent=parent) + + if controller is None: + controller = BaseWorkfileController() + + self.setWindowTitle(self.title) + icon = QtGui.QIcon(resources.get_openpype_icon_filepath()) + self.setWindowIcon(icon) + flags = self.windowFlags() | QtCore.Qt.Window + self.setWindowFlags(flags) + + self._default_window_flags = flags + + self._folder_widget = None + self._folder_filter_input = None + + self._files_widget = None + + self._first_show = True + self._controller_refreshed = False + self._context_to_set = None + # Host validation should happen only once + self._host_is_valid = None + + self._controller = controller + + # Create pages widget and set it as central widget + pages_widget = QtWidgets.QStackedWidget(self) + + home_page_widget = QtWidgets.QWidget(pages_widget) + home_body_widget = QtWidgets.QWidget(home_page_widget) + + col_1_widget = self._create_col_1_widget(controller, parent) + tasks_widget = TasksWidget(controller, home_body_widget) + col_3_widget = self._create_col_3_widget(controller, home_body_widget) + side_panel = SidePanelWidget(controller, home_body_widget) + + pages_widget.addWidget(home_page_widget) + + # Build home + home_page_layout = QtWidgets.QVBoxLayout(home_page_widget) + home_page_layout.addWidget(home_body_widget) + + # Build home - body + body_layout = QtWidgets.QVBoxLayout(home_body_widget) + split_widget = QtWidgets.QSplitter(home_body_widget) + split_widget.addWidget(col_1_widget) + split_widget.addWidget(tasks_widget) + split_widget.addWidget(col_3_widget) + split_widget.addWidget(side_panel) + split_widget.setSizes([255, 160, 455, 175]) + + body_layout.addWidget(split_widget) + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.addWidget(pages_widget, 1) + + overlay_messages_widget = MessageOverlayObject(self) + overlay_invalid_host = InvalidHostOverlay(self) + overlay_invalid_host.setVisible(False) + + first_show_timer = QtCore.QTimer() + first_show_timer.setSingleShot(True) + first_show_timer.setInterval(50) + + first_show_timer.timeout.connect(self._on_first_show) + + controller.register_event_callback( + "save_as.finished", + self._on_save_as_finished, + ) + controller.register_event_callback( + "copy_representation.finished", + self._on_copy_representation_finished, + ) + controller.register_event_callback( + "workfile_duplicate.finished", + self._on_duplicate_finished + ) + controller.register_event_callback( + "open_workfile.finished", + self._on_open_finished + ) + controller.register_event_callback( + "controller.refresh.started", + self._on_controller_refresh_started, + ) + controller.register_event_callback( + "controller.refresh.finished", + self._on_controller_refresh_finished, + ) + + self._overlay_messages_widget = overlay_messages_widget + self._overlay_invalid_host = overlay_invalid_host + self._home_page_widget = home_page_widget + self._pages_widget = pages_widget + self._home_body_widget = home_body_widget + self._split_widget = split_widget + + self._tasks_widget = tasks_widget + self._side_panel = side_panel + + self._first_show_timer = first_show_timer + + self._post_init() + + def _post_init(self): + self._on_published_checkbox_changed() + + # Force focus on the open button by default, required for Houdini. + self._files_widget.setFocus() + + self.resize(1200, 600) + + def _create_col_1_widget(self, controller, parent): + col_widget = QtWidgets.QWidget(parent) + header_widget = QtWidgets.QWidget(col_widget) + + folder_filter_input = PlaceholderLineEdit(header_widget) + folder_filter_input.setPlaceholderText("Filter folders..") + + go_to_current_btn = QtWidgets.QPushButton(header_widget) + go_to_current_btn.setIcon(get_go_to_current_icon()) + go_to_current_btn_sp = go_to_current_btn.sizePolicy() + go_to_current_btn_sp.setVerticalPolicy(QtWidgets.QSizePolicy.Minimum) + go_to_current_btn.setSizePolicy(go_to_current_btn_sp) + + refresh_btn = QtWidgets.QPushButton(header_widget) + refresh_btn.setIcon(get_refresh_icon()) + refresh_btn_sp = refresh_btn.sizePolicy() + refresh_btn_sp.setVerticalPolicy(QtWidgets.QSizePolicy.Minimum) + refresh_btn.setSizePolicy(refresh_btn_sp) + + folder_widget = FoldersWidget(controller, col_widget) + + header_layout = QtWidgets.QHBoxLayout(header_widget) + header_layout.setContentsMargins(0, 0, 0, 0) + header_layout.addWidget(folder_filter_input, 1) + header_layout.addWidget(go_to_current_btn, 0) + header_layout.addWidget(refresh_btn, 0) + + col_layout = QtWidgets.QVBoxLayout(col_widget) + col_layout.setContentsMargins(0, 0, 0, 0) + col_layout.addWidget(header_widget, 0) + col_layout.addWidget(folder_widget, 1) + + folder_filter_input.textChanged.connect(self._on_folder_filter_change) + go_to_current_btn.clicked.connect(self._on_go_to_current_clicked) + refresh_btn.clicked.connect(self._on_refresh_clicked) + + self._folder_filter_input = folder_filter_input + self._folder_widget = folder_widget + + return col_widget + + def _create_col_3_widget(self, controller, parent): + col_widget = QtWidgets.QWidget(parent) + + header_widget = QtWidgets.QWidget(col_widget) + + files_filter_input = PlaceholderLineEdit(header_widget) + files_filter_input.setPlaceholderText("Filter files..") + + published_checkbox = QtWidgets.QCheckBox("Published", header_widget) + published_checkbox.setToolTip("Show published workfiles") + + header_layout = QtWidgets.QHBoxLayout(header_widget) + header_layout.setContentsMargins(0, 0, 0, 0) + header_layout.addWidget(files_filter_input, 1) + header_layout.addWidget(published_checkbox, 0) + + files_widget = FilesWidget(controller, col_widget) + + col_layout = QtWidgets.QVBoxLayout(col_widget) + col_layout.setContentsMargins(0, 0, 0, 0) + col_layout.addWidget(header_widget, 0) + col_layout.addWidget(files_widget, 1) + + files_filter_input.textChanged.connect( + self._on_file_text_filter_change) + published_checkbox.stateChanged.connect( + self._on_published_checkbox_changed + ) + + self._files_filter_input = files_filter_input + self._published_checkbox = published_checkbox + + self._files_widget = files_widget + + return col_widget + + def set_window_on_top(self, on_top): + """Set window on top of other windows. + + Args: + on_top (bool): Show on top of other windows. + """ + + flags = self._default_window_flags + if on_top: + flags |= QtCore.Qt.WindowStaysOnTopHint + if self.windowFlags() != flags: + self.setWindowFlags(flags) + + def ensure_visible(self, use_context=True, save=True, on_top=False): + """Ensure the window is visible. + + This method expects arguments for compatibility with previous variant + of Workfiles tool. + + Args: + use_context (Optional[bool]): DEPRECATED: This argument is + ignored. + save (Optional[bool]): Allow to save workfiles. + on_top (Optional[bool]): Show on top of other windows. + """ + + save = True if save is None else save + on_top = False if on_top is None else on_top + + is_visible = self.isVisible() + self._controller.set_save_enabled(save) + self.set_window_on_top(on_top) + + self.show() + self.raise_() + self.activateWindow() + if is_visible: + self.refresh() + + def refresh(self): + """Trigger refresh of workfiles tool controller.""" + + self._controller.refresh() + + def showEvent(self, event): + super(WorkfilesToolWindow, self).showEvent(event) + if self._first_show: + self._first_show = False + self._first_show_timer.start() + self.setStyleSheet(style.load_stylesheet()) + + def keyPressEvent(self, event): + """Custom keyPressEvent. + + Override keyPressEvent to do nothing so that Maya's panels won't + take focus when pressing "SHIFT" whilst mouse is over viewport or + outliner. This way users don't accidentally perform Maya commands + whilst trying to name an instance. + """ + + pass + + def _on_first_show(self): + if not self._controller_refreshed: + self.refresh() + + def _on_file_text_filter_change(self, text): + self._files_widget.set_text_filter(text) + + def _on_published_checkbox_changed(self): + """Publish mode changed. + + Tell children widgets about it so they can handle the mode. + """ + + published_mode = self._published_checkbox.isChecked() + self._files_widget.set_published_mode(published_mode) + self._side_panel.set_published_mode(published_mode) + + def _on_folder_filter_change(self, text): + self._folder_widget.set_name_filer(text) + + def _on_go_to_current_clicked(self): + self._controller.go_to_current_context() + + def _on_refresh_clicked(self): + self.refresh() + + def _on_controller_refresh_started(self): + self._controller_refreshed = True + + def _on_controller_refresh_finished(self): + if self._host_is_valid is None: + self._host_is_valid = self._controller.is_host_valid() + self._overlay_invalid_host.setVisible(not self._host_is_valid) + + if not self._host_is_valid: + return + + def _on_save_as_finished(self, event): + if event["failed"]: + self._overlay_messages_widget.add_message( + "Failed to save workfile", + "error", + ) + else: + self._overlay_messages_widget.add_message( + "Workfile saved" + ) + + def _on_copy_representation_finished(self, event): + if event["failed"]: + self._overlay_messages_widget.add_message( + "Failed to copy published workfile", + "error", + ) + else: + self._overlay_messages_widget.add_message( + "Publish workfile saved" + ) + + def _on_duplicate_finished(self, event): + if event["failed"]: + self._overlay_messages_widget.add_message( + "Failed to duplicate workfile", + "error", + ) + else: + self._overlay_messages_widget.add_message( + "Workfile duplicated" + ) + + def _on_open_finished(self, event): + if event["failed"]: + self._overlay_messages_widget.add_message( + "Failed to open workfile", + "error", + ) + else: + self.close() diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py index bc4b7867c2..2ebc973a47 100644 --- a/openpype/tools/utils/host_tools.py +++ b/openpype/tools/utils/host_tools.py @@ -6,6 +6,8 @@ use singleton approach with global functions (using helper anyway). import os import pyblish.api + +from openpype import AYON_SERVER_ENABLED from openpype.host import IWorkfileHost, ILoadHost from openpype.lib import Logger from openpype.pipeline import ( @@ -46,17 +48,29 @@ class HostToolsHelper: self._log = Logger.get_logger(self.__class__.__name__) return self._log + def _init_ayon_workfiles_tool(self, parent): + from openpype.tools.ayon_workfiles.widgets import WorkfilesToolWindow + + workfiles_window = WorkfilesToolWindow(parent=parent) + self._workfiles_tool = workfiles_window + + def _init_openpype_workfiles_tool(self, parent): + from openpype.tools.workfiles.app import Window + + # Host validation + host = registered_host() + IWorkfileHost.validate_workfile_methods(host) + + workfiles_window = Window(parent=parent) + self._workfiles_tool = workfiles_window + def get_workfiles_tool(self, parent): """Create, cache and return workfiles tool window.""" if self._workfiles_tool is None: - from openpype.tools.workfiles.app import Window - - # Host validation - host = registered_host() - IWorkfileHost.validate_workfile_methods(host) - - workfiles_window = Window(parent=parent) - self._workfiles_tool = workfiles_window + if AYON_SERVER_ENABLED: + self._init_ayon_workfiles_tool(parent) + else: + self._init_openpype_workfiles_tool(parent) return self._workfiles_tool From 5ba400d89173195cadf885df3c5bc0129c84fd61 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 9 Sep 2023 03:24:54 +0000 Subject: [PATCH 326/327] [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 d5d46bab0c..b6c56296bc 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.16.5" +__version__ = "3.16.6-nightly.1" From 5475908051c3166daa57155c18b36f672170e12b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 9 Sep 2023 03:25:32 +0000 Subject: [PATCH 327/327] 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 a35dbf1a17..7a39103859 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.16.6-nightly.1 - 3.16.5 - 3.16.5-nightly.5 - 3.16.5-nightly.4 @@ -134,7 +135,6 @@ body: - 3.14.9-nightly.4 - 3.14.9-nightly.3 - 3.14.9-nightly.2 - - 3.14.9-nightly.1 validations: required: true - type: dropdown